fork of https://github.com/sourcegraph/zoekt
0

Configure Feed

Select the types of activity you want to include in your feed.

Multiline "chunk" matches (#367)

This adds an option to return multiline "chunk" matches from the Zoekt API rather than splitting matches by line.

+2417 -734
+53 -3
api.go
··· 39 39 40 40 // Repository is the globally unique name of the repo of the 41 41 // match 42 - Repository string 43 - Branches []string 44 - LineMatches []LineMatch 42 + Repository string 43 + Branches []string 44 + 45 + // One of LineMatches or ChunkMatches will be returned depending on whether 46 + // the SearchOptions.ChunkMatches is set. 47 + LineMatches []LineMatch 48 + ChunkMatches []ChunkMatch 45 49 46 50 // RepositoryID is a Sourcegraph extension. This is the ID of Repository in 47 51 // Sourcegraph. ··· 70 74 71 75 // Commit SHA1 (hex) of the (sub)repo holding the file. 72 76 Version string 77 + } 78 + 79 + // ChunkMatch is a set of non-overlapping matches within a contiguous range of 80 + // lines in the file. 81 + type ChunkMatch struct { 82 + // Content is a contiguous range of complete lines that fully contains Ranges. 83 + Content []byte 84 + // ContentStart is the location (inclusive) of the beginning of content 85 + // relative to the beginning of the file. It will always be at the 86 + // beginning of a line (Column will always be 1). 87 + ContentStart Location 88 + 89 + // FileName indicates whether this match is a match on the file name, in 90 + // which case Content will contain the file name. 91 + FileName bool 92 + 93 + // Ranges is a set of matching ranges within this chunk. Each range is relative 94 + // to the beginning of the file (not the beginning of Content). 95 + Ranges []Range 96 + 97 + // SymbolInfo is the symbol information associated with Ranges. If it is non-nil, 98 + // its length will equal that of Ranges. Any of its elements may be nil. 99 + SymbolInfo []*Symbol 100 + 101 + Score float64 102 + DebugScore string 103 + } 104 + 105 + type Range struct { 106 + // The inclusive beginning of the range. 107 + Start Location 108 + // The exclusive end of the range. 109 + End Location 110 + } 111 + 112 + type Location struct { 113 + // 0-based byte offset from the beginning of the file 114 + ByteOffset uint32 115 + // 1-based line number from the beginning of the file 116 + LineNumber uint32 117 + // 1-based column number (in runes) from the beginning of line 118 + Column uint32 73 119 } 74 120 75 121 // LineMatch holds the matches within a single line in a file. ··· 544 590 // Note that the included context lines might contain matches and 545 591 // it's up to the consumer of the result to remove those lines. 546 592 NumContextLines int 593 + 594 + // If true, ChunkMatches will be returned in each FileMatch rather than LineMatches 595 + // EXPERIMENTAL: the behavior of this flag may be changed in future versions. 596 + ChunkMatches bool 547 597 548 598 // Trace turns on opentracing for this request if true and if the Jaeger address was provided as 549 599 // a command-line flag
+234
contentprovider.go
··· 163 163 return result 164 164 } 165 165 166 + func (p *contentProvider) fillChunkMatches(ms []*candidateMatch, numContextLines int, language string, debug bool) []ChunkMatch { 167 + var result []ChunkMatch 168 + if ms[0].fileName { 169 + // If the first match is a filename match, there will only be 170 + // one match and the matched content will be the filename. 171 + 172 + fileName := p.id.fileName(p.idx) 173 + ranges := make([]Range, 0, len(ms)) 174 + for _, m := range ms { 175 + ranges = append(ranges, Range{ 176 + Start: Location{ 177 + ByteOffset: m.byteOffset, 178 + LineNumber: 1, 179 + Column: uint32(utf8.RuneCount(fileName[:m.byteOffset]) + 1), 180 + }, 181 + End: Location{ 182 + ByteOffset: m.byteOffset + m.byteMatchSz, 183 + LineNumber: 1, 184 + Column: uint32(utf8.RuneCount(fileName[:m.byteOffset+m.byteMatchSz]) + 1), 185 + }, 186 + }) 187 + } 188 + 189 + result = []ChunkMatch{{ 190 + Content: fileName, 191 + ContentStart: Location{ByteOffset: 0, LineNumber: 1, Column: 1}, 192 + Ranges: ranges, 193 + FileName: true, 194 + }} 195 + } else { 196 + result = p.fillContentChunkMatches(ms, numContextLines) 197 + } 198 + 199 + sects := p.docSections() 200 + for i, m := range result { 201 + result[i].Score, result[i].DebugScore = p.chunkMatchScore(sects, &m, language, debug) 202 + } 203 + 204 + return result 205 + } 206 + 166 207 func (p *contentProvider) fillContentMatches(ms []*candidateMatch, numContextLines int) []LineMatch { 167 208 var result []LineMatch 168 209 for len(ms) > 0 { ··· 241 282 return result 242 283 } 243 284 285 + func (p *contentProvider) fillContentChunkMatches(ms []*candidateMatch, numContextLines int) []ChunkMatch { 286 + newlines := p.newlines() 287 + chunks := chunkCandidates(ms, newlines, numContextLines) 288 + data := p.data(false) 289 + chunkMatches := make([]ChunkMatch, 0, len(chunks)) 290 + for _, chunk := range chunks { 291 + ranges := make([]Range, 0, len(chunk.candidates)) 292 + var symbolInfo []*Symbol 293 + for i, cm := range chunk.candidates { 294 + startOffset := cm.byteOffset 295 + endOffset := cm.byteOffset + cm.byteMatchSz 296 + startLine, startLineOffset, _ := newlines.atOffset(startOffset) 297 + endLine, endLineOffset, _ := newlines.atOffset(endOffset) 298 + 299 + ranges = append(ranges, Range{ 300 + Start: Location{ 301 + ByteOffset: startOffset, 302 + LineNumber: uint32(startLine), 303 + Column: uint32(utf8.RuneCount(data[startLineOffset:startOffset]) + 1), 304 + }, 305 + End: Location{ 306 + ByteOffset: endOffset, 307 + LineNumber: uint32(endLine), 308 + Column: uint32(utf8.RuneCount(data[endLineOffset:endOffset]) + 1), 309 + }, 310 + }) 311 + 312 + if cm.symbol { 313 + if symbolInfo == nil { 314 + symbolInfo = make([]*Symbol, len(chunk.candidates)) 315 + } 316 + start := p.id.fileEndSymbol[p.idx] 317 + si := p.id.symbols.data(start + cm.symbolIdx) 318 + if si != nil { 319 + sec := p.docSections()[cm.symbolIdx] 320 + si.Sym = string(data[sec.Start:sec.End]) 321 + } 322 + symbolInfo[i] = si 323 + } 324 + } 325 + 326 + firstLineNumber := int(chunk.firstLine) - numContextLines 327 + if firstLineNumber < 1 { 328 + firstLineNumber = 1 329 + } 330 + firstLineStart, _ := newlines.lineBounds(firstLineNumber) 331 + 332 + chunkMatches = append(chunkMatches, ChunkMatch{ 333 + Content: newlines.getLines(data, firstLineNumber, int(chunk.lastLine)+numContextLines+1), 334 + ContentStart: Location{ 335 + ByteOffset: firstLineStart, 336 + LineNumber: uint32(firstLineNumber), 337 + Column: 1, 338 + }, 339 + FileName: false, 340 + Ranges: ranges, 341 + SymbolInfo: symbolInfo, 342 + }) 343 + } 344 + return chunkMatches 345 + } 346 + 347 + type candidateChunk struct { 348 + firstLine uint32 // 1-based, inclusive 349 + lastLine uint32 // 1-based, inclusive 350 + minOffset uint32 // 0-based, inclusive 351 + maxOffset uint32 // 0-based, exclusive 352 + candidates []*candidateMatch 353 + } 354 + 355 + // chunkCandidates groups a set of sorted, non-overlapping candidate matches by line number. Adjacent 356 + // chunks will be merged if adding `numContextLines` to the beginning and end of the chunk would cause 357 + // it to overlap with an adjacent chunk. 358 + func chunkCandidates(ms []*candidateMatch, newlines newlines, numContextLines int) []candidateChunk { 359 + var chunks []candidateChunk 360 + for _, m := range ms { 361 + startOffset := m.byteOffset 362 + endOffset := m.byteOffset + m.byteMatchSz 363 + firstLine, _, _ := newlines.atOffset(startOffset) 364 + lastLine, _, _ := newlines.atOffset(endOffset) 365 + 366 + if len(chunks) > 0 && int(chunks[len(chunks)-1].lastLine)+numContextLines >= firstLine-numContextLines { 367 + // If a new chunk created with the current candidateMatch would 368 + // overlap with the previous chunk, instead add the candidateMatch 369 + // to the last chunk and extend end of the last chunk. 370 + last := &chunks[len(chunks)-1] 371 + last.candidates = append(last.candidates, m) 372 + if last.maxOffset < endOffset { 373 + last.lastLine = uint32(lastLine) 374 + last.maxOffset = uint32(endOffset) 375 + } 376 + } else { 377 + chunks = append(chunks, candidateChunk{ 378 + firstLine: uint32(firstLine), 379 + lastLine: uint32(lastLine), 380 + minOffset: startOffset, 381 + maxOffset: endOffset, 382 + candidates: []*candidateMatch{m}, 383 + }) 384 + } 385 + } 386 + return chunks 387 + } 388 + 244 389 type newlines struct { 245 390 // locs is the sorted set of byte offsets of the newlines in the file 246 391 locs []uint32 ··· 339 484 return 0, false 340 485 } 341 486 487 + func (p *contentProvider) chunkMatchScore(secs []DocumentSection, m *ChunkMatch, language string, debug bool) (float64, string) { 488 + type debugScore struct { 489 + score float64 490 + what string 491 + } 492 + 493 + score := &debugScore{} 494 + maxScore := &debugScore{} 495 + 496 + addScore := func(what string, s float64) { 497 + if debug { 498 + score.what += fmt.Sprintf("%s:%f, ", what, s) 499 + } 500 + score.score += s 501 + } 502 + 503 + for i, r := range m.Ranges { 504 + // calculate the start and end offset relative to the start of the content 505 + relStartOffset := int(r.Start.ByteOffset - m.ContentStart.ByteOffset) 506 + relEndOffset := int(r.End.ByteOffset - m.ContentStart.ByteOffset) 507 + 508 + startBoundary := relStartOffset < len(m.Content) && (relStartOffset == 0 || byteClass(m.Content[relStartOffset-1]) != byteClass(m.Content[relStartOffset])) 509 + endBoundary := relEndOffset > 0 && (relEndOffset == len(m.Content) || byteClass(m.Content[relEndOffset-1]) != byteClass(m.Content[relEndOffset])) 510 + 511 + score.score = 0 512 + score.what = "" 513 + 514 + if startBoundary && endBoundary { 515 + addScore("WordMatch", scoreWordMatch) 516 + } else if startBoundary || endBoundary { 517 + addScore("PartialWordMatch", scorePartialWordMatch) 518 + } 519 + 520 + if m.FileName { 521 + sep := bytes.LastIndexByte(m.Content, '/') 522 + startMatch := relStartOffset == sep+1 523 + endMatch := relEndOffset == len(m.Content) 524 + if startMatch && endMatch { 525 + addScore("Base", scoreBase) 526 + } else if startMatch || endMatch { 527 + addScore("EdgeBase", (scoreBase+scorePartialBase)/2) 528 + } else if sep < relStartOffset { 529 + addScore("InnerBase", scorePartialBase) 530 + } 531 + } else if secIdx, ok := findSection(secs, uint32(r.Start.ByteOffset), uint32(r.End.ByteOffset-r.Start.ByteOffset)); ok { 532 + sec := secs[secIdx] 533 + startMatch := sec.Start == uint32(r.Start.ByteOffset) 534 + endMatch := sec.End == uint32(r.End.ByteOffset) 535 + if startMatch && endMatch { 536 + addScore("Symbol", scoreSymbol) 537 + } else if startMatch || endMatch { 538 + addScore("EdgeSymbol", (scoreSymbol+scorePartialSymbol)/2) 539 + } else { 540 + addScore("InnerSymbol", scorePartialSymbol) 541 + } 542 + 543 + var si *Symbol 544 + if m.SymbolInfo != nil { 545 + si = m.SymbolInfo[i] 546 + } 547 + if si == nil { 548 + // for non-symbol queries, we need to hydrate in SymbolInfo. 549 + start := p.id.fileEndSymbol[p.idx] 550 + si = p.id.symbols.data(start + uint32(secIdx)) 551 + } 552 + if si != nil { 553 + addScore(fmt.Sprintf("kind:%s:%s", language, si.Kind), scoreKind(language, si.Kind)) 554 + } 555 + } 556 + 557 + if score.score > maxScore.score { 558 + maxScore.score = score.score 559 + maxScore.what = score.what 560 + } 561 + } 562 + 563 + return maxScore.score, strings.TrimRight(maxScore.what, ", ") 564 + } 565 + 342 566 func (p *contentProvider) matchScore(secs []DocumentSection, m *LineMatch, language string, debug bool) (float64, string) { 343 567 type debugScore struct { 344 568 score float64 ··· 437 661 func (m matchScoreSlice) Swap(i, j int) { m[i], m[j] = m[j], m[i] } 438 662 func (m matchScoreSlice) Less(i, j int) bool { return m[i].Score > m[j].Score } 439 663 664 + type chunkMatchScoreSlice []ChunkMatch 665 + 666 + func (m chunkMatchScoreSlice) Len() int { return len(m) } 667 + func (m chunkMatchScoreSlice) Swap(i, j int) { m[i], m[j] = m[j], m[i] } 668 + func (m chunkMatchScoreSlice) Less(i, j int) bool { return m[i].Score > m[j].Score } 669 + 440 670 type fileMatchSlice []FileMatch 441 671 442 672 func (m fileMatchSlice) Len() int { return len(m) } ··· 445 675 446 676 func sortMatchesByScore(ms []LineMatch) { 447 677 sort.Sort(matchScoreSlice(ms)) 678 + } 679 + 680 + func sortChunkMatchesByScore(ms []ChunkMatch) { 681 + sort.Sort(chunkMatchScoreSlice(ms)) 448 682 } 449 683 450 684 // Sort a slice of results.
+136
contentprovider_test.go
··· 2 2 3 3 import ( 4 4 "bytes" 5 + "fmt" 5 6 "testing" 6 7 7 8 "github.com/google/go-cmp/cmp" ··· 190 191 }) 191 192 } 192 193 } 194 + 195 + func TestChunkMatches(t *testing.T) { 196 + content := []byte(`0.2.4.6.8.10. 197 + 13.16.19.22. 198 + 26.29.32.35. 199 + 39.42.45.48. 200 + 52.55.58.61. 201 + 65.68.71.74. 202 + 78.81.84.87. 203 + `) 204 + match_0_2 := &candidateMatch{byteOffset: 0, byteMatchSz: 2} 205 + match_6_10 := &candidateMatch{byteOffset: 6, byteMatchSz: 4} 206 + match_10_16 := &candidateMatch{byteOffset: 10, byteMatchSz: 6} 207 + match_19_42 := &candidateMatch{byteOffset: 19, byteMatchSz: 23} 208 + match_45_48 := &candidateMatch{byteOffset: 45, byteMatchSz: 3} 209 + match_71_72 := &candidateMatch{byteOffset: 71, byteMatchSz: 1} 210 + 211 + cases := []struct { 212 + candidateMatches []*candidateMatch 213 + numContextLines int 214 + want []candidateChunk 215 + }{{ 216 + candidateMatches: []*candidateMatch{match_0_2}, 217 + numContextLines: 0, 218 + want: []candidateChunk{{ 219 + firstLine: 1, 220 + minOffset: 0, 221 + lastLine: 1, 222 + maxOffset: 2, 223 + candidates: []*candidateMatch{match_0_2}, 224 + }}, 225 + }, { 226 + candidateMatches: []*candidateMatch{match_0_2}, 227 + numContextLines: 5, 228 + want: []candidateChunk{{ 229 + firstLine: 1, 230 + minOffset: 0, 231 + lastLine: 1, 232 + maxOffset: 2, 233 + candidates: []*candidateMatch{match_0_2}, 234 + }}, 235 + }, { 236 + candidateMatches: []*candidateMatch{match_0_2, match_6_10}, 237 + numContextLines: 0, 238 + want: []candidateChunk{{ 239 + firstLine: 1, 240 + minOffset: 0, 241 + lastLine: 1, 242 + maxOffset: 10, 243 + candidates: []*candidateMatch{match_0_2, match_6_10}, 244 + }}, 245 + }, { 246 + candidateMatches: []*candidateMatch{match_0_2, match_10_16}, 247 + numContextLines: 0, 248 + want: []candidateChunk{{ 249 + firstLine: 1, 250 + minOffset: 0, 251 + lastLine: 2, 252 + maxOffset: 16, 253 + candidates: []*candidateMatch{match_0_2, match_10_16}, 254 + }}, 255 + }, { 256 + candidateMatches: []*candidateMatch{match_0_2, match_19_42}, 257 + numContextLines: 0, 258 + want: []candidateChunk{{ 259 + firstLine: 1, 260 + minOffset: 0, 261 + lastLine: 1, 262 + maxOffset: 2, 263 + candidates: []*candidateMatch{match_0_2}, 264 + }, { 265 + firstLine: 2, 266 + minOffset: 19, 267 + lastLine: 4, 268 + maxOffset: 42, 269 + candidates: []*candidateMatch{match_19_42}, 270 + }}, 271 + }, { 272 + candidateMatches: []*candidateMatch{match_0_2, match_19_42}, 273 + numContextLines: 1, 274 + want: []candidateChunk{{ 275 + firstLine: 1, 276 + minOffset: 0, 277 + lastLine: 4, 278 + maxOffset: 42, 279 + candidates: []*candidateMatch{match_0_2, match_19_42}, 280 + }}, 281 + }, { 282 + candidateMatches: []*candidateMatch{ 283 + match_0_2, match_19_42, match_45_48, match_71_72, 284 + }, 285 + numContextLines: 0, 286 + want: []candidateChunk{{ 287 + firstLine: 1, 288 + minOffset: 0, 289 + lastLine: 1, 290 + maxOffset: 2, 291 + candidates: []*candidateMatch{match_0_2}, 292 + }, { 293 + firstLine: 2, 294 + minOffset: 19, 295 + lastLine: 4, 296 + maxOffset: 48, 297 + candidates: []*candidateMatch{match_19_42, match_45_48}, 298 + }, { 299 + firstLine: 6, 300 + minOffset: 71, 301 + lastLine: 6, 302 + maxOffset: 72, 303 + candidates: []*candidateMatch{match_71_72}, 304 + }}, 305 + }, { 306 + candidateMatches: []*candidateMatch{ 307 + match_0_2, match_19_42, match_45_48, match_71_72, 308 + }, 309 + numContextLines: 100, 310 + want: []candidateChunk{{ 311 + firstLine: 1, 312 + minOffset: 0, 313 + lastLine: 6, 314 + maxOffset: 72, 315 + candidates: []*candidateMatch{match_0_2, match_19_42, match_45_48, match_71_72}, 316 + }}, 317 + }} 318 + 319 + newlines := getNewlines(content) 320 + for _, tt := range cases { 321 + t.Run("", func(t *testing.T) { 322 + got := chunkCandidates(tt.candidateMatches, newlines, tt.numContextLines) 323 + if diff := cmp.Diff(fmt.Sprintf("%#v\n", tt.want), fmt.Sprintf("%#v\n", got)); diff != "" { 324 + t.Fatal(diff) 325 + } 326 + }) 327 + } 328 + }
+66 -19
eval.go
··· 322 322 visitMatches(mt, known, func(mt matchTree) { 323 323 atomMatchCount++ 324 324 }) 325 - finalCands := gatherMatches(mt, known) 325 + shouldMergeMatches := !opts.ChunkMatches 326 + finalCands := gatherMatches(mt, known, shouldMergeMatches) 326 327 327 328 if len(finalCands) == 0 { 328 329 nm := d.fileName(nextDoc) ··· 338 339 byteMatchSz: uint32(len(nm)), 339 340 }) 340 341 } 341 - fileMatch.LineMatches = cp.fillMatches(finalCands, opts.NumContextLines, fileMatch.Language, opts.DebugScore) 342 + 343 + if opts.ChunkMatches { 344 + fileMatch.ChunkMatches = cp.fillChunkMatches(finalCands, opts.NumContextLines, fileMatch.Language, opts.DebugScore) 345 + } else { 346 + fileMatch.LineMatches = cp.fillMatches(finalCands, opts.NumContextLines, fileMatch.Language, opts.DebugScore) 347 + } 342 348 343 349 maxFileScore := 0.0 344 350 for i := range fileMatch.LineMatches { ··· 350 356 fileMatch.LineMatches[i].Score += scoreLineOrderFactor * (1.0 - (float64(i) / float64(len(fileMatch.LineMatches)))) 351 357 } 352 358 359 + for i := range fileMatch.ChunkMatches { 360 + if maxFileScore < fileMatch.ChunkMatches[i].Score { 361 + maxFileScore = fileMatch.ChunkMatches[i].Score 362 + } 363 + 364 + // Order by ordering in file. 365 + fileMatch.ChunkMatches[i].Score += scoreLineOrderFactor * (1.0 - (float64(i) / float64(len(fileMatch.ChunkMatches)))) 366 + } 367 + 353 368 // Maintain ordering of input files. This 354 369 // strictly dominates the in-file ordering of 355 370 // the matches. ··· 365 380 } 366 381 fileMatch.Branches = d.gatherBranches(nextDoc, mt, known) 367 382 sortMatchesByScore(fileMatch.LineMatches) 383 + sortChunkMatchesByScore(fileMatch.ChunkMatches) 368 384 if opts.Whole { 369 385 fileMatch.Content = cp.data(false) 370 386 } 371 387 388 + matchedChunkRanges := 0 389 + for _, cm := range fileMatch.ChunkMatches { 390 + matchedChunkRanges += len(cm.Ranges) 391 + } 392 + 372 393 repoMatchCount += len(fileMatch.LineMatches) 394 + repoMatchCount += matchedChunkRanges 373 395 374 396 res.Files = append(res.Files, fileMatch) 375 397 res.Stats.MatchCount += len(fileMatch.LineMatches) 398 + res.Stats.MatchCount += matchedChunkRanges 376 399 res.Stats.FileCount++ 377 400 } 378 401 ··· 420 443 // filename/content matches: if there are content matches, all 421 444 // filename matches are trimmed from the result. The matches are 422 445 // returned in document order and are non-overlapping. 423 - func gatherMatches(mt matchTree, known map[matchTree]bool) []*candidateMatch { 446 + // 447 + // If `merge` is set, overlapping and adjacent matches will be merged 448 + // into a single match. Otherwise, overlapping matches will be removed, 449 + // but adjacent matches will remain. 450 + func gatherMatches(mt matchTree, known map[matchTree]bool, merge bool) []*candidateMatch { 424 451 var cands []*candidateMatch 425 452 visitMatches(mt, known, func(mt matchTree) { 426 453 if smt, ok := mt.(*substrMatchTree); ok { ··· 450 477 } 451 478 cands = res 452 479 453 - // Merge adjacent candidates. This guarantees that the matches 454 - // are non-overlapping. 455 - sort.Sort((sortByOffsetSlice)(cands)) 456 - res = cands[:0] 457 - for i, c := range cands { 458 - if i == 0 { 480 + if merge { 481 + // Merge adjacent candidates. This guarantees that the matches 482 + // are non-overlapping. 483 + sort.Sort((sortByOffsetSlice)(cands)) 484 + res = cands[:0] 485 + for i, c := range cands { 486 + if i == 0 { 487 + res = append(res, c) 488 + continue 489 + } 490 + last := res[len(res)-1] 491 + lastEnd := last.byteOffset + last.byteMatchSz 492 + end := c.byteOffset + c.byteMatchSz 493 + if lastEnd >= c.byteOffset { 494 + if end > lastEnd { 495 + last.byteMatchSz = end - last.byteOffset 496 + } 497 + continue 498 + } 499 + 459 500 res = append(res, c) 460 - continue 461 501 } 462 - last := res[len(res)-1] 463 - lastEnd := last.byteOffset + last.byteMatchSz 464 - end := c.byteOffset + c.byteMatchSz 465 - if lastEnd >= c.byteOffset { 466 - if end > lastEnd { 467 - last.byteMatchSz = end - last.byteOffset 502 + } else { 503 + // Remove overlapping candidates. This guarantees that the matches 504 + // are non-overlapping, but also preserves expected match counts. 505 + sort.Sort((sortByOffsetSlice)(cands)) 506 + res = cands[:0] 507 + for i, c := range cands { 508 + if i == 0 { 509 + res = append(res, c) 510 + continue 511 + } 512 + last := res[len(res)-1] 513 + lastEnd := last.byteOffset + last.byteMatchSz 514 + if lastEnd > c.byteOffset { 515 + continue 468 516 } 469 - continue 517 + 518 + res = append(res, c) 470 519 } 471 - 472 - res = append(res, c) 473 520 } 474 521 475 522 return res
+1925 -712
index_test.go
··· 37 37 for j := range r.Files[i].LineMatches { 38 38 r.Files[i].LineMatches[j].Score = 0.0 39 39 } 40 + for j := range r.Files[i].ChunkMatches { 41 + r.Files[i].ChunkMatches[j].Score = 0.0 42 + } 40 43 r.Files[i].Checksum = nil 41 44 r.Files[i].Debug = "" 42 45 } ··· 154 157 // --------------0123456789012345678901234567890123 155 158 }) 156 159 157 - res := searchForTest(t, b, &query.Substring{ 158 - Pattern: "water", 159 - CaseSensitive: true, 160 + t.Run("LineMatch", func(t *testing.T) { 161 + res := searchForTest(t, b, &query.Substring{ 162 + Pattern: "water", 163 + CaseSensitive: true, 164 + }) 165 + fmatches := res.Files 166 + if len(fmatches) != 1 || len(fmatches[0].LineMatches) != 1 { 167 + t.Fatalf("got %v, want 1 matches", fmatches) 168 + } 169 + 170 + got := fmt.Sprintf("%s:%d", fmatches[0].FileName, fmatches[0].LineMatches[0].LineFragments[0].Offset) 171 + want := "f2:9" 172 + if got != want { 173 + t.Errorf("1: got %s, want %s", got, want) 174 + } 160 175 }) 161 - fmatches := res.Files 162 - if len(fmatches) != 1 || len(fmatches[0].LineMatches) != 1 { 163 - t.Fatalf("got %v, want 1 matches", fmatches) 164 - } 165 176 166 - got := fmt.Sprintf("%s:%d", fmatches[0].FileName, fmatches[0].LineMatches[0].LineFragments[0].Offset) 167 - want := "f2:9" 168 - if got != want { 169 - t.Errorf("1: got %s, want %s", got, want) 170 - } 177 + t.Run("ChunkMatch", func(t *testing.T) { 178 + res := searchForTest(t, b, &query.Substring{ 179 + Pattern: "water", 180 + CaseSensitive: true, 181 + }, chunkOpts) 182 + fmatches := res.Files 183 + if len(fmatches) != 1 || len(fmatches[0].ChunkMatches) != 1 { 184 + t.Fatalf("got %v, want 1 matches", fmatches) 185 + } 186 + 187 + got := fmt.Sprintf("%s:%d", fmatches[0].FileName, fmatches[0].ChunkMatches[0].Ranges[0].Start.ByteOffset) 188 + want := "f2:9" 189 + if got != want { 190 + t.Errorf("1: got %s, want %s", got, want) 191 + } 192 + }) 171 193 } 172 194 173 195 func TestEmptyIndex(t *testing.T) { ··· 208 230 func TestNewlines(t *testing.T) { 209 231 b := testIndexBuilder(t, nil, 210 232 Document{Name: "filename", Content: []byte("line1\nline2\nbla")}) 211 - // -------------------------------------------------012345-678901-234 233 + // ---------------------------------------------012345-678901-234 212 234 213 - sres := searchForTest(t, b, &query.Substring{Pattern: "ne2"}) 235 + t.Run("LineMatches", func(t *testing.T) { 236 + sres := searchForTest(t, b, &query.Substring{Pattern: "ne2"}) 214 237 215 - matches := sres.Files 216 - want := []FileMatch{{ 217 - FileName: "filename", 218 - LineMatches: []LineMatch{ 219 - { 238 + matches := sres.Files 239 + want := []FileMatch{{ 240 + FileName: "filename", 241 + LineMatches: []LineMatch{{ 220 242 LineFragments: []LineFragmentMatch{{ 221 243 Offset: 8, 222 244 LineOffset: 2, ··· 226 248 LineStart: 6, 227 249 LineEnd: 11, 228 250 LineNumber: 2, 229 - }, 230 - }, 231 - }} 251 + }}, 252 + }} 253 + 254 + if !reflect.DeepEqual(matches, want) { 255 + t.Errorf("got %v, want %v", matches, want) 256 + } 257 + }) 258 + 259 + t.Run("ChunkMatches", func(t *testing.T) { 260 + sres := searchForTest(t, b, &query.Substring{Pattern: "ne2"}, chunkOpts) 261 + 262 + matches := sres.Files 263 + want := []FileMatch{{ 264 + FileName: "filename", 265 + ChunkMatches: []ChunkMatch{{ 266 + Content: []byte("line2"), 267 + ContentStart: Location{ 268 + ByteOffset: 6, 269 + LineNumber: 2, 270 + Column: 1, 271 + }, 272 + Ranges: []Range{{ 273 + Start: Location{ByteOffset: 8, LineNumber: 2, Column: 3}, 274 + End: Location{ByteOffset: 11, LineNumber: 2, Column: 6}, 275 + }}, 276 + }}, 277 + }} 232 278 233 - if !reflect.DeepEqual(matches, want) { 234 - t.Errorf("got %v, want %v", matches, want) 235 - } 279 + if diff := cmp.Diff(want, matches); diff != "" { 280 + t.Fatal(diff) 281 + } 282 + }) 236 283 } 237 284 238 285 // A result spanning multiple lines should have LineMatches that only cover ··· 241 288 text := "line1\nline2\nbla" 242 289 b := testIndexBuilder(t, nil, 243 290 Document{Name: "filename", Content: []byte(text)}) 244 - sres := searchForTest(t, b, &query.Substring{Pattern: "ine2\nbla"}) 245 - matches := sres.Files 246 - if len(matches) != 1 { 247 - t.Fatalf("got %d file matches, want exactly one", len(matches)) 248 - } 249 - m := matches[0] 250 - if len(m.LineMatches) != 2 { 251 - t.Fatalf("got %d line matches, want exactly two", len(m.LineMatches)) 252 - } 291 + 292 + t.Run("LineMatches", func(t *testing.T) { 293 + sres := searchForTest(t, b, &query.Substring{Pattern: "ine2\nbla"}) 294 + matches := sres.Files 295 + if len(matches) != 1 { 296 + t.Fatalf("got %d file matches, want exactly one", len(matches)) 297 + } 298 + m := matches[0] 299 + if len(m.LineMatches) != 2 { 300 + t.Fatalf("got %d line matches, want exactly two", len(m.LineMatches)) 301 + } 302 + }) 303 + 304 + t.Run("ChunkMatches", func(t *testing.T) { 305 + sres := searchForTest(t, b, &query.Substring{Pattern: "ine2\nbla"}, chunkOpts) 306 + matches := sres.Files 307 + if len(matches) != 1 { 308 + t.Fatalf("got %d file matches, want exactly one", len(matches)) 309 + } 310 + m := matches[0] 311 + if len(m.ChunkMatches) != 1 { 312 + t.Fatalf("got %d chunk matches, want exactly one", len(m.ChunkMatches)) 313 + } 314 + }) 253 315 } 316 + 317 + var chunkOpts = SearchOptions{ChunkMatches: true} 254 318 255 319 func searchForTest(t *testing.T, b *IndexBuilder, q query.Q, o ...SearchOptions) *SearchResult { 256 320 searcher := searcherForTest(t, b) ··· 286 350 Document{Name: "f1", Content: []byte("I love BaNaNAS.")}, 287 351 // -----------------------------------012345678901234 288 352 ) 289 - sres := searchForTest(t, b, &query.Substring{ 290 - Pattern: "bananas", 291 - CaseSensitive: true, 353 + t.Run("LineMatches", func(t *testing.T) { 354 + sres := searchForTest(t, b, &query.Substring{ 355 + Pattern: "bananas", 356 + CaseSensitive: true, 357 + }) 358 + matches := sres.Files 359 + if len(matches) != 0 { 360 + t.Errorf("foldcase: got %#v, want 0 matches", matches) 361 + } 362 + 363 + sres = searchForTest(t, b, 364 + &query.Substring{ 365 + Pattern: "BaNaNAS", 366 + CaseSensitive: true, 367 + }) 368 + matches = sres.Files 369 + if len(matches) != 1 { 370 + t.Errorf("no foldcase: got %v, want 1 matches", matches) 371 + } else if matches[0].LineMatches[0].LineFragments[0].Offset != 7 { 372 + t.Errorf("foldcase: got %v, want offsets 7", matches) 373 + } 292 374 }) 293 - matches := sres.Files 294 - if len(matches) != 0 { 295 - t.Errorf("foldcase: got %#v, want 0 matches", matches) 296 - } 297 375 298 - sres = searchForTest(t, b, 299 - &query.Substring{ 300 - Pattern: "BaNaNAS", 376 + t.Run("ChunkMatches", func(t *testing.T) { 377 + sres := searchForTest(t, b, &query.Substring{ 378 + Pattern: "bananas", 301 379 CaseSensitive: true, 302 - }) 303 - matches = sres.Files 304 - if len(matches) != 1 { 305 - t.Errorf("no foldcase: got %v, want 1 matches", matches) 306 - } else if matches[0].LineMatches[0].LineFragments[0].Offset != 7 { 307 - t.Errorf("foldcase: got %v, want offsets 7", matches) 308 - } 380 + }, chunkOpts) 381 + matches := sres.Files 382 + if len(matches) != 0 { 383 + t.Errorf("foldcase: got %#v, want 0 matches", matches) 384 + } 385 + 386 + sres = searchForTest(t, b, 387 + &query.Substring{ 388 + Pattern: "BaNaNAS", 389 + CaseSensitive: true, 390 + }) 391 + matches = sres.Files 392 + if len(matches) != 1 { 393 + t.Errorf("no foldcase: got %v, want 1 matches", matches) 394 + } else if matches[0].LineMatches[0].LineFragments[0].Offset != 7 { 395 + t.Errorf("foldcase: got %v, want offsets 7", matches) 396 + } 397 + }) 309 398 } 310 399 311 400 func TestAndSearch(t *testing.T) { ··· 313 402 Document{Name: "f1", Content: []byte("x banana y")}, 314 403 Document{Name: "f2", Content: []byte("x apple y")}, 315 404 Document{Name: "f3", Content: []byte("x banana apple y")}, 316 - // -------------------------------------------0123456789012345 405 + // ---------------------------------------0123456789012345 317 406 ) 318 - sres := searchForTest(t, b, query.NewAnd( 319 - &query.Substring{ 320 - Pattern: "banana", 321 - }, 322 - &query.Substring{ 323 - Pattern: "apple", 324 - }, 325 - )) 326 - matches := sres.Files 327 - if len(matches) != 1 || len(matches[0].LineMatches) != 1 || len(matches[0].LineMatches[0].LineFragments) != 2 { 328 - t.Fatalf("got %#v, want 1 match with 2 fragments", matches) 329 - } 407 + 408 + t.Run("LineMatches", func(t *testing.T) { 409 + sres := searchForTest(t, b, query.NewAnd( 410 + &query.Substring{ 411 + Pattern: "banana", 412 + }, 413 + &query.Substring{ 414 + Pattern: "apple", 415 + }, 416 + )) 417 + matches := sres.Files 418 + if len(matches) != 1 || len(matches[0].LineMatches) != 1 || len(matches[0].LineMatches[0].LineFragments) != 2 { 419 + t.Fatalf("got %#v, want 1 match with 2 fragments", matches) 420 + } 421 + 422 + if matches[0].LineMatches[0].LineFragments[0].Offset != 2 || matches[0].LineMatches[0].LineFragments[1].Offset != 9 { 423 + t.Fatalf("got %#v, want offsets 2,9", matches) 424 + } 425 + 426 + wantStats := Stats{ 427 + FilesLoaded: 1, 428 + ContentBytesLoaded: 18, 429 + IndexBytesLoaded: 8, 430 + NgramMatches: 3, // we look at doc 1, because it's max(0,1) due to AND 431 + MatchCount: 1, 432 + FileCount: 1, 433 + FilesConsidered: 2, 434 + ShardsScanned: 1, 435 + } 436 + if diff := pretty.Compare(wantStats, sres.Stats); diff != "" { 437 + t.Errorf("got stats diff %s", diff) 438 + } 439 + }) 440 + 441 + t.Run("ChunkMatches", func(t *testing.T) { 442 + sres := searchForTest(t, b, query.NewAnd( 443 + &query.Substring{ 444 + Pattern: "banana", 445 + }, 446 + &query.Substring{ 447 + Pattern: "apple", 448 + }, 449 + ), chunkOpts) 450 + matches := sres.Files 451 + if len(matches) != 1 || len(matches[0].ChunkMatches) != 1 || len(matches[0].ChunkMatches[0].Ranges) != 2 { 452 + t.Fatalf("got %#v, want 1 chunk match with 2 ranges", matches) 453 + } 330 454 331 - if matches[0].LineMatches[0].LineFragments[0].Offset != 2 || matches[0].LineMatches[0].LineFragments[1].Offset != 9 { 332 - t.Fatalf("got %#v, want offsets 2,9", matches) 333 - } 455 + if matches[0].ChunkMatches[0].Ranges[0].Start.ByteOffset != 2 || matches[0].ChunkMatches[0].Ranges[1].Start.ByteOffset != 9 { 456 + t.Fatalf("got %#v, want offsets 2,9", matches) 457 + } 334 458 335 - wantStats := Stats{ 336 - FilesLoaded: 1, 337 - ContentBytesLoaded: 18, 338 - IndexBytesLoaded: 8, 339 - NgramMatches: 3, // we look at doc 1, because it's max(0,1) due to AND 340 - MatchCount: 1, 341 - FileCount: 1, 342 - FilesConsidered: 2, 343 - ShardsScanned: 1, 344 - } 345 - if diff := pretty.Compare(wantStats, sres.Stats); diff != "" { 346 - t.Errorf("got stats diff %s", diff) 347 - } 459 + wantStats := Stats{ 460 + FilesLoaded: 1, 461 + ContentBytesLoaded: 18, 462 + IndexBytesLoaded: 8, 463 + NgramMatches: 3, // we look at doc 1, because it's max(0,1) due to AND 464 + MatchCount: 2, 465 + FileCount: 1, 466 + FilesConsidered: 2, 467 + ShardsScanned: 1, 468 + } 469 + if diff := pretty.Compare(wantStats, sres.Stats); diff != "" { 470 + t.Errorf("got stats diff %s", diff) 471 + } 472 + }) 348 473 } 349 474 350 475 func TestAndNegateSearch(t *testing.T) { ··· 352 477 Document{Name: "f1", Content: []byte("x banana y")}, 353 478 // -----------------------------------0123456789 354 479 Document{Name: "f4", Content: []byte("x banana apple y")}) 355 - sres := searchForTest(t, b, query.NewAnd( 356 - &query.Substring{ 357 - Pattern: "banana", 358 - }, 359 - &query.Not{Child: &query.Substring{ 360 - Pattern: "apple", 361 - }})) 480 + 481 + t.Run("LineMatches", func(t *testing.T) { 482 + sres := searchForTest(t, b, query.NewAnd( 483 + &query.Substring{ 484 + Pattern: "banana", 485 + }, 486 + &query.Not{Child: &query.Substring{ 487 + Pattern: "apple", 488 + }})) 489 + 490 + matches := sres.Files 362 491 363 - matches := sres.Files 492 + if len(matches) != 1 || len(matches[0].LineMatches) != 1 { 493 + t.Fatalf("got %v, want 1 match", matches) 494 + } 495 + if matches[0].FileName != "f1" { 496 + t.Fatalf("got match %#v, want FileName: f1", matches[0]) 497 + } 498 + if matches[0].LineMatches[0].LineFragments[0].Offset != 2 { 499 + t.Fatalf("got %v, want offset 2", matches) 500 + } 501 + }) 364 502 365 - if len(matches) != 1 || len(matches[0].LineMatches) != 1 { 366 - t.Fatalf("got %v, want 1 match", matches) 367 - } 368 - if matches[0].FileName != "f1" { 369 - t.Fatalf("got match %#v, want FileName: f1", matches[0]) 370 - } 371 - if matches[0].LineMatches[0].LineFragments[0].Offset != 2 { 372 - t.Fatalf("got %v, want offset 2", matches) 373 - } 503 + t.Run("ChunkMatches", func(t *testing.T) { 504 + sres := searchForTest(t, b, 505 + query.NewAnd( 506 + &query.Substring{ 507 + Pattern: "banana", 508 + }, 509 + &query.Not{Child: &query.Substring{ 510 + Pattern: "apple", 511 + }}, 512 + ), 513 + chunkOpts, 514 + ) 515 + 516 + matches := sres.Files 517 + 518 + if len(matches) != 1 || len(matches[0].ChunkMatches) != 1 { 519 + t.Fatalf("got %v, want 1 match", matches) 520 + } 521 + if matches[0].FileName != "f1" { 522 + t.Fatalf("got match %#v, want FileName: f1", matches[0]) 523 + } 524 + if matches[0].ChunkMatches[0].Ranges[0].Start.ByteOffset != 2 { 525 + t.Fatalf("got %v, want offset 2", matches) 526 + } 527 + }) 374 528 } 375 529 376 530 func TestNegativeMatchesOnlyShortcut(t *testing.T) { ··· 380 534 Document{Name: "f3", Content: []byte("x appelmoes y")}, 381 535 Document{Name: "f3", Content: []byte("x appelmoes y")}) 382 536 383 - sres := searchForTest(t, b, query.NewAnd( 384 - &query.Substring{ 385 - Pattern: "banana", 386 - }, 387 - &query.Not{Child: &query.Substring{ 388 - Pattern: "appel", 389 - }})) 537 + t.Run("LineMatches", func(t *testing.T) { 538 + sres := searchForTest(t, b, query.NewAnd( 539 + &query.Substring{ 540 + Pattern: "banana", 541 + }, 542 + &query.Not{Child: &query.Substring{ 543 + Pattern: "appel", 544 + }})) 545 + 546 + if sres.Stats.FilesConsidered != 1 { 547 + t.Errorf("got %#v, want FilesConsidered: 1", sres.Stats) 548 + } 549 + }) 550 + 551 + t.Run("ChunkMatches", func(t *testing.T) { 552 + sres := searchForTest(t, b, query.NewAnd( 553 + &query.Substring{ 554 + Pattern: "banana", 555 + }, 556 + &query.Not{Child: &query.Substring{ 557 + Pattern: "appel", 558 + }}), chunkOpts) 390 559 391 - if sres.Stats.FilesConsidered != 1 { 392 - t.Errorf("got %#v, want FilesConsidered: 1", sres.Stats) 393 - } 560 + if sres.Stats.FilesConsidered != 1 { 561 + t.Errorf("got %#v, want FilesConsidered: 1", sres.Stats) 562 + } 563 + }) 394 564 } 395 565 396 566 func TestFileSearch(t *testing.T) { ··· 400 570 Document{Name: "banana", Content: []byte("x apple y")}, 401 571 // -------------012345 402 572 ) 403 - sres := searchForTest(t, b, &query.Substring{ 404 - Pattern: "anan", 405 - FileName: true, 573 + 574 + t.Run("LineMatches", func(t *testing.T) { 575 + sres := searchForTest(t, b, &query.Substring{ 576 + Pattern: "anan", 577 + FileName: true, 578 + }) 579 + 580 + matches := sres.Files 581 + if len(matches) != 1 || len(matches[0].LineMatches) != 1 { 582 + t.Fatalf("got %v, want 1 match", matches) 583 + } 584 + 585 + got := matches[0].LineMatches[0] 586 + want := LineMatch{ 587 + Line: []byte("banana"), 588 + LineFragments: []LineFragmentMatch{{ 589 + Offset: 1, 590 + LineOffset: 1, 591 + MatchLength: 4, 592 + }}, 593 + FileName: true, 594 + } 595 + 596 + if !reflect.DeepEqual(got, want) { 597 + t.Errorf("got %#v, want %#v", got, want) 598 + } 406 599 }) 407 600 408 - matches := sres.Files 409 - if len(matches) != 1 || len(matches[0].LineMatches) != 1 { 410 - t.Fatalf("got %v, want 1 match", matches) 411 - } 601 + t.Run("ChunkMatches", func(t *testing.T) { 602 + sres := searchForTest(t, b, &query.Substring{ 603 + Pattern: "anan", 604 + FileName: true, 605 + }, chunkOpts) 606 + 607 + matches := sres.Files 608 + if len(matches) != 1 || len(matches[0].ChunkMatches) != 1 { 609 + t.Fatalf("got %v, want 1 match", matches) 610 + } 412 611 413 - got := matches[0].LineMatches[0] 414 - want := LineMatch{ 415 - Line: []byte("banana"), 416 - LineFragments: []LineFragmentMatch{{ 417 - Offset: 1, 418 - LineOffset: 1, 419 - MatchLength: 4, 420 - }}, 421 - FileName: true, 422 - } 612 + got := matches[0].ChunkMatches[0] 613 + want := ChunkMatch{ 614 + Content: []byte("banana"), 615 + ContentStart: Location{ByteOffset: 0, LineNumber: 1, Column: 1}, 616 + Ranges: []Range{{ 617 + Start: Location{ByteOffset: 1, LineNumber: 1, Column: 2}, 618 + End: Location{ByteOffset: 5, LineNumber: 1, Column: 6}, 619 + }}, 620 + FileName: true, 621 + } 423 622 424 - if !reflect.DeepEqual(got, want) { 425 - t.Errorf("got %#v, want %#v", got, want) 426 - } 623 + if diff := cmp.Diff(want, got); diff != "" { 624 + t.Fatal(diff) 625 + } 626 + }) 427 627 } 428 628 429 629 func TestFileCase(t *testing.T) { 430 630 b := testIndexBuilder(t, nil, 431 631 Document{Name: "BANANA", Content: []byte("x orange y")}) 432 - sres := searchForTest(t, b, &query.Substring{ 433 - Pattern: "banana", 434 - FileName: true, 632 + 633 + t.Run("LineMatches", func(t *testing.T) { 634 + sres := searchForTest(t, b, &query.Substring{ 635 + Pattern: "banana", 636 + FileName: true, 637 + }) 638 + 639 + matches := sres.Files 640 + if len(matches) != 1 || matches[0].FileName != "BANANA" { 641 + t.Fatalf("got %v, want 1 match 'BANANA'", matches) 642 + } 435 643 }) 436 644 437 - matches := sres.Files 438 - if len(matches) != 1 || matches[0].FileName != "BANANA" { 439 - t.Fatalf("got %v, want 1 match 'BANANA'", matches) 440 - } 645 + t.Run("ChunkMatches", func(t *testing.T) { 646 + sres := searchForTest(t, b, &query.Substring{ 647 + Pattern: "banana", 648 + FileName: true, 649 + }, chunkOpts) 650 + 651 + matches := sres.Files 652 + if len(matches) != 1 || matches[0].FileName != "BANANA" { 653 + t.Fatalf("got %v, want 1 match 'BANANA'", matches) 654 + } 655 + }) 441 656 } 442 657 443 658 func TestFileRegexpSearchBruteForce(t *testing.T) { ··· 445 660 Document{Name: "banzana", Content: []byte("x orange y")}, 446 661 Document{Name: "banana", Content: []byte("x apple y")}, 447 662 ) 448 - sres := searchForTest(t, b, &query.Regexp{ 449 - Regexp: mustParseRE("[qn][zx]"), 450 - FileName: true, 663 + t.Run("LineMatches", func(t *testing.T) { 664 + sres := searchForTest(t, b, &query.Regexp{ 665 + Regexp: mustParseRE("[qn][zx]"), 666 + FileName: true, 667 + }) 668 + 669 + matches := sres.Files 670 + if len(matches) != 1 || matches[0].FileName != "banzana" { 671 + t.Fatalf("got %v, want 1 match on 'banzana'", matches) 672 + } 451 673 }) 674 + t.Run("LineMatches", func(t *testing.T) { 675 + sres := searchForTest(t, b, &query.Regexp{ 676 + Regexp: mustParseRE("[qn][zx]"), 677 + FileName: true, 678 + }, chunkOpts) 452 679 453 - matches := sres.Files 454 - if len(matches) != 1 || matches[0].FileName != "banzana" { 455 - t.Fatalf("got %v, want 1 match on 'banzana'", matches) 456 - } 680 + matches := sres.Files 681 + if len(matches) != 1 || matches[0].FileName != "banzana" { 682 + t.Fatalf("got %v, want 1 match on 'banzana'", matches) 683 + } 684 + }) 457 685 } 458 686 459 687 func TestFileRegexpSearchShortString(t *testing.T) { 460 688 b := testIndexBuilder(t, nil, 461 689 Document{Name: "banana.py", Content: []byte("x orange y")}) 462 - sres := searchForTest(t, b, &query.Regexp{ 463 - Regexp: mustParseRE("ana.py"), 464 - FileName: true, 690 + 691 + t.Run("LineMatches", func(t *testing.T) { 692 + sres := searchForTest(t, b, &query.Regexp{ 693 + Regexp: mustParseRE("ana.py"), 694 + FileName: true, 695 + }) 696 + 697 + matches := sres.Files 698 + if len(matches) != 1 || matches[0].FileName != "banana.py" { 699 + t.Fatalf("got %v, want 1 match on 'banana.py'", matches) 700 + } 465 701 }) 466 702 467 - matches := sres.Files 468 - if len(matches) != 1 || matches[0].FileName != "banana.py" { 469 - t.Fatalf("got %v, want 1 match on 'banana.py'", matches) 470 - } 703 + t.Run("ChunkMatches", func(t *testing.T) { 704 + sres := searchForTest(t, b, &query.Regexp{ 705 + Regexp: mustParseRE("ana.py"), 706 + FileName: true, 707 + }, chunkOpts) 708 + 709 + matches := sres.Files 710 + if len(matches) != 1 || matches[0].FileName != "banana.py" { 711 + t.Fatalf("got %v, want 1 match on 'banana.py'", matches) 712 + } 713 + }) 471 714 } 472 715 473 716 func TestFileSubstringSearchBruteForce(t *testing.T) { ··· 480 723 FileName: true, 481 724 } 482 725 483 - res := searchForTest(t, b, q) 484 - if len(res.Files) != 1 || res.Files[0].FileName != "BANZANA" { 485 - t.Fatalf("got %v, want 1 match on 'BANZANA''", res.Files) 486 - } 726 + t.Run("LineMatches", func(t *testing.T) { 727 + res := searchForTest(t, b, q) 728 + if len(res.Files) != 1 || res.Files[0].FileName != "BANZANA" { 729 + t.Fatalf("got %v, want 1 match on 'BANZANA''", res.Files) 730 + } 731 + }) 732 + 733 + t.Run("ChunkMatches", func(t *testing.T) { 734 + res := searchForTest(t, b, q, chunkOpts) 735 + if len(res.Files) != 1 || res.Files[0].FileName != "BANZANA" { 736 + t.Fatalf("got %v, want 1 match on 'BANZANA''", res.Files) 737 + } 738 + }) 487 739 } 488 740 489 741 func TestFileSubstringSearchBruteForceEnd(t *testing.T) { ··· 495 747 Pattern: "q", 496 748 FileName: true, 497 749 } 750 + t.Run("LineMatches", func(t *testing.T) { 751 + res := searchForTest(t, b, q) 752 + if want := "bananaq"; len(res.Files) != 1 || res.Files[0].FileName != want { 753 + t.Fatalf("got %v, want 1 match in %q", res.Files, want) 754 + } 755 + }) 498 756 499 - res := searchForTest(t, b, q) 500 - if want := "bananaq"; len(res.Files) != 1 || res.Files[0].FileName != want { 501 - t.Fatalf("got %v, want 1 match in %q", res.Files, want) 502 - } 757 + t.Run("LineMatches", func(t *testing.T) { 758 + res := searchForTest(t, b, q, chunkOpts) 759 + if want := "bananaq"; len(res.Files) != 1 || res.Files[0].FileName != want { 760 + t.Fatalf("got %v, want 1 match in %q", res.Files, want) 761 + } 762 + }) 503 763 } 504 764 505 765 func TestSearchMatchAll(t *testing.T) { 506 766 b := testIndexBuilder(t, nil, 507 767 Document{Name: "banzana", Content: []byte("x orange y")}, 508 768 Document{Name: "banana", Content: []byte("x apple y")}) 509 - sres := searchForTest(t, b, &query.Const{Value: true}) 769 + 770 + t.Run("LineMatches", func(t *testing.T) { 771 + sres := searchForTest(t, b, &query.Const{Value: true}) 772 + matches := sres.Files 773 + if len(matches) != 2 { 774 + t.Fatalf("got %v, want 2 matches", matches) 775 + } 776 + }) 510 777 511 - matches := sres.Files 512 - if len(matches) != 2 { 513 - t.Fatalf("got %v, want 2 matches", matches) 514 - } 778 + t.Run("ChunkMatches", func(t *testing.T) { 779 + sres := searchForTest(t, b, &query.Const{Value: true}, chunkOpts) 780 + matches := sres.Files 781 + if len(matches) != 2 { 782 + t.Fatalf("got %v, want 2 matches", matches) 783 + } 784 + }) 515 785 } 516 786 517 787 func TestSearchNewline(t *testing.T) { 518 788 b := testIndexBuilder(t, nil, 519 789 Document{Name: "banzana", Content: []byte("abcd\ndefg")}) 520 - sres := searchForTest(t, b, &query.Substring{Pattern: "d\nd"}) 790 + 791 + t.Run("LineMatches", func(t *testing.T) { 792 + sres := searchForTest(t, b, &query.Substring{Pattern: "d\nd"}) 793 + 794 + // Just check that we don't crash. 795 + 796 + matches := sres.Files 797 + if len(matches) != 1 { 798 + t.Fatalf("got %v, want 1 matches", matches) 799 + } 800 + }) 521 801 522 - // Just check that we don't crash. 802 + t.Run("ChunkMatches", func(t *testing.T) { 803 + sres := searchForTest(t, b, &query.Substring{Pattern: "d\nd"}, chunkOpts) 523 804 524 - matches := sres.Files 525 - if len(matches) != 1 { 526 - t.Fatalf("got %v, want 1 matches", matches) 527 - } 805 + // Just check that we don't crash. 806 + 807 + matches := sres.Files 808 + if len(matches) != 1 { 809 + t.Fatalf("got %v, want 1 matches", matches) 810 + } 811 + }) 528 812 } 529 813 530 814 func TestSearchMatchAllRegexp(t *testing.T) { 531 815 b := testIndexBuilder(t, nil, 532 816 Document{Name: "banzana", Content: []byte("abcd")}, 533 817 Document{Name: "banana", Content: []byte("pqrs")}) 534 - sres := searchForTest(t, b, &query.Regexp{Regexp: mustParseRE(".")}) 535 818 536 - matches := sres.Files 537 - if len(matches) != 2 || sres.Stats.MatchCount != 2 { 538 - t.Fatalf("got %v, want 2 matches", matches) 539 - } 540 - if len(matches[0].LineMatches[0].Line) != 4 || len(matches[1].LineMatches[0].Line) != 4 { 541 - t.Fatalf("want 4 chars in every file, got %#v", matches) 542 - } 819 + t.Run("LineMatches", func(t *testing.T) { 820 + sres := searchForTest(t, b, &query.Regexp{Regexp: mustParseRE(".")}) 821 + 822 + matches := sres.Files 823 + if len(matches) != 2 || sres.Stats.MatchCount != 2 { 824 + t.Fatalf("got %v, want 2 matches", matches) 825 + } 826 + if len(matches[0].LineMatches[0].Line) != 4 || len(matches[1].LineMatches[0].Line) != 4 { 827 + t.Fatalf("want 4 chars in every file, got %#v", matches) 828 + } 829 + 830 + }) 831 + 832 + t.Run("ChunkMatches", func(t *testing.T) { 833 + sres := searchForTest(t, b, &query.Regexp{Regexp: mustParseRE(".")}, chunkOpts) 834 + 835 + matches := sres.Files 836 + if len(matches) != 2 || sres.Stats.MatchCount != 8 { 837 + t.Fatalf("got %v, want 2 matches", matches) 838 + } 839 + if len(matches[0].ChunkMatches[0].Content) != 4 || len(matches[1].ChunkMatches[0].Content) != 4 { 840 + t.Fatalf("want 4 chars in every file, got %#v", matches) 841 + } 842 + 843 + }) 543 844 } 544 845 545 846 func TestFileRestriction(t *testing.T) { ··· 547 848 Document{Name: "banana1", Content: []byte("x orange y")}, 548 849 Document{Name: "banana2", Content: []byte("x apple y")}, 549 850 Document{Name: "orange", Content: []byte("x apple z")}) 550 - sres := searchForTest(t, b, query.NewAnd( 551 - &query.Substring{ 552 - Pattern: "banana", 553 - FileName: true, 554 - }, 555 - &query.Substring{ 556 - Pattern: "apple", 557 - })) 851 + 852 + t.Run("LineMatches", func(t *testing.T) { 853 + sres := searchForTest(t, b, query.NewAnd( 854 + &query.Substring{ 855 + Pattern: "banana", 856 + FileName: true, 857 + }, 858 + &query.Substring{ 859 + Pattern: "apple", 860 + })) 558 861 559 - matches := sres.Files 560 - if len(matches) != 1 || len(matches[0].LineMatches) != 1 { 561 - t.Fatalf("got %v, want 1 match", matches) 562 - } 862 + matches := sres.Files 863 + if len(matches) != 1 || len(matches[0].LineMatches) != 1 { 864 + t.Fatalf("got %v, want 1 match", matches) 865 + } 563 866 564 - match := matches[0].LineMatches[0] 565 - got := string(match.Line) 566 - want := "x apple y" 567 - if got != want { 568 - t.Errorf("got match %#v, want line %q", match, want) 569 - } 867 + match := matches[0].LineMatches[0] 868 + got := string(match.Line) 869 + want := "x apple y" 870 + if got != want { 871 + t.Errorf("got match %#v, want line %q", match, want) 872 + } 873 + }) 874 + 875 + t.Run("ChunkMatches", func(t *testing.T) { 876 + sres := searchForTest(t, b, query.NewAnd( 877 + &query.Substring{ 878 + Pattern: "banana", 879 + FileName: true, 880 + }, 881 + &query.Substring{ 882 + Pattern: "apple", 883 + }), chunkOpts) 884 + 885 + matches := sres.Files 886 + if len(matches) != 1 || len(matches[0].ChunkMatches) != 1 { 887 + t.Fatalf("got %v, want 1 match", matches) 888 + } 889 + 890 + match := matches[0].ChunkMatches[0] 891 + got := string(match.Content) 892 + want := "x apple y" 893 + if got != want { 894 + t.Errorf("got match %#v, want line %q", match, want) 895 + } 896 + }) 570 897 } 571 898 572 899 func TestFileNameBoundary(t *testing.T) { ··· 574 901 Document{Name: "banana2", Content: []byte("x apple y")}, 575 902 Document{Name: "helpers.go", Content: []byte("x apple y")}, 576 903 Document{Name: "foo", Content: []byte("x apple y")}) 577 - sres := searchForTest(t, b, &query.Substring{ 578 - Pattern: "helpers.go", 579 - FileName: true, 904 + 905 + t.Run("LineMatches", func(t *testing.T) { 906 + sres := searchForTest(t, b, &query.Substring{ 907 + Pattern: "helpers.go", 908 + FileName: true, 909 + }) 910 + 911 + matches := sres.Files 912 + if len(matches) != 1 || len(matches[0].LineMatches) != 1 { 913 + t.Fatalf("got %v, want 1 match", matches) 914 + } 580 915 }) 581 916 582 - matches := sres.Files 583 - if len(matches) != 1 || len(matches[0].LineMatches) != 1 { 584 - t.Fatalf("got %v, want 1 match", matches) 585 - } 917 + t.Run("ChunkMatches", func(t *testing.T) { 918 + sres := searchForTest(t, b, &query.Substring{ 919 + Pattern: "helpers.go", 920 + FileName: true, 921 + }, chunkOpts) 922 + 923 + matches := sres.Files 924 + if len(matches) != 1 || len(matches[0].ChunkMatches) != 1 { 925 + t.Fatalf("got %v, want 1 match", matches) 926 + } 927 + }) 586 928 } 587 929 588 930 func TestDocumentOrder(t *testing.T) { ··· 593 935 594 936 b := testIndexBuilder(t, nil, docs...) 595 937 596 - sres := searchForTest(t, b, query.NewAnd( 597 - &query.Substring{ 598 - Pattern: "needle", 599 - })) 938 + t.Run("LineMatches", func(t *testing.T) { 939 + sres := searchForTest(t, b, query.NewAnd( 940 + &query.Substring{ 941 + Pattern: "needle", 942 + })) 943 + 944 + want := []string{"f0", "f1", "f2"} 945 + var got []string 946 + for _, f := range sres.Files { 947 + got = append(got, f.FileName) 948 + } 949 + if !reflect.DeepEqual(got, want) { 950 + t.Fatalf("got %v, want %v", got, want) 951 + } 952 + }) 953 + 954 + t.Run("ChunkMatches", func(t *testing.T) { 955 + sres := searchForTest(t, b, 956 + query.NewAnd(&query.Substring{ 957 + Pattern: "needle", 958 + }), 959 + chunkOpts, 960 + ) 600 961 601 - want := []string{"f0", "f1", "f2"} 602 - var got []string 603 - for _, f := range sres.Files { 604 - got = append(got, f.FileName) 605 - } 606 - if !reflect.DeepEqual(got, want) { 607 - t.Fatalf("got %v, want %v", got, want) 608 - } 962 + want := []string{"f0", "f1", "f2"} 963 + var got []string 964 + for _, f := range sres.Files { 965 + got = append(got, f.FileName) 966 + } 967 + if !reflect.DeepEqual(got, want) { 968 + t.Fatalf("got %v, want %v", got, want) 969 + } 970 + }) 609 971 } 610 972 611 973 func TestBranchMask(t *testing.T) { ··· 621 983 Document{Name: "f4", Content: []byte("needle"), Branches: []string{"bonzai"}}, 622 984 ) 623 985 624 - sres := searchForTest(t, b, query.NewAnd( 625 - &query.Substring{ 626 - Pattern: "needle", 627 - }, 628 - &query.Branch{ 629 - Pattern: "table", 630 - })) 986 + t.Run("LineMatches", func(t *testing.T) { 987 + sres := searchForTest(t, b, query.NewAnd( 988 + &query.Substring{ 989 + Pattern: "needle", 990 + }, 991 + &query.Branch{ 992 + Pattern: "table", 993 + })) 994 + 995 + if len(sres.Files) != 2 || sres.Files[0].FileName != "f2" || sres.Files[1].FileName != "f3" { 996 + t.Fatalf("got %v, want 2 result from [f2,f3]", sres.Files) 997 + } 631 998 632 - if len(sres.Files) != 2 || sres.Files[0].FileName != "f2" || sres.Files[1].FileName != "f3" { 633 - t.Fatalf("got %v, want 2 result from [f2,f3]", sres.Files) 634 - } 999 + if len(sres.Files[0].Branches) != 1 || sres.Files[0].Branches[0] != "stable" { 1000 + t.Fatalf("got %v, want 1 branch 'stable'", sres.Files[0].Branches) 1001 + } 1002 + }) 635 1003 636 - if len(sres.Files[0].Branches) != 1 || sres.Files[0].Branches[0] != "stable" { 637 - t.Fatalf("got %v, want 1 branch 'stable'", sres.Files[0].Branches) 638 - } 1004 + t.Run("ChunkMatches", func(t *testing.T) { 1005 + sres := searchForTest(t, b, query.NewAnd( 1006 + &query.Substring{ 1007 + Pattern: "needle", 1008 + }, 1009 + &query.Branch{ 1010 + Pattern: "table", 1011 + }), 1012 + chunkOpts, 1013 + ) 1014 + 1015 + if len(sres.Files) != 2 || sres.Files[0].FileName != "f2" || sres.Files[1].FileName != "f3" { 1016 + t.Fatalf("got %v, want 2 result from [f2,f3]", sres.Files) 1017 + } 1018 + 1019 + if len(sres.Files[0].Branches) != 1 || sres.Files[0].Branches[0] != "stable" { 1020 + t.Fatalf("got %v, want 1 branch 'stable'", sres.Files[0].Branches) 1021 + } 1022 + }) 639 1023 } 640 1024 641 1025 func TestBranchLimit(t *testing.T) { ··· 665 1049 }, 666 1050 }, 667 1051 Document{Name: "f2", Content: []byte("needle"), Branches: branches}) 668 - sres := searchForTest(t, b, &query.Substring{ 669 - Pattern: "needle", 1052 + 1053 + t.Run("LineMatches", func(t *testing.T) { 1054 + sres := searchForTest(t, b, &query.Substring{ 1055 + Pattern: "needle", 1056 + }) 1057 + if len(sres.Files) != 1 { 1058 + t.Fatalf("got %v, want 1 result from f2", sres.Files) 1059 + } 1060 + 1061 + f := sres.Files[0] 1062 + if !reflect.DeepEqual(f.Branches, branches) { 1063 + t.Fatalf("got branches %q, want %q", f.Branches, branches) 1064 + } 670 1065 }) 671 - if len(sres.Files) != 1 { 672 - t.Fatalf("got %v, want 1 result from f2", sres.Files) 673 - } 1066 + 1067 + t.Run("ChunkMatches", func(t *testing.T) { 1068 + sres := searchForTest(t, b, &query.Substring{ 1069 + Pattern: "needle", 1070 + }, chunkOpts) 1071 + if len(sres.Files) != 1 { 1072 + t.Fatalf("got %v, want 1 result from f2", sres.Files) 1073 + } 674 1074 675 - f := sres.Files[0] 676 - if !reflect.DeepEqual(f.Branches, branches) { 677 - t.Fatalf("got branches %q, want %q", f.Branches, branches) 678 - } 1075 + f := sres.Files[0] 1076 + if !reflect.DeepEqual(f.Branches, branches) { 1077 + t.Fatalf("got branches %q, want %q", f.Branches, branches) 1078 + } 1079 + }) 1080 + 679 1081 } 680 1082 681 1083 func TestBranchVersions(t *testing.T) { ··· 686 1088 }, 687 1089 }, Document{Name: "f2", Content: []byte("needle"), Branches: []string{"master"}}) 688 1090 689 - sres := searchForTest(t, b, &query.Substring{ 690 - Pattern: "needle", 1091 + t.Run("LineMatches", func(t *testing.T) { 1092 + sres := searchForTest(t, b, &query.Substring{ 1093 + Pattern: "needle", 1094 + }) 1095 + if len(sres.Files) != 1 { 1096 + t.Fatalf("got %v, want 1 result from f2", sres.Files) 1097 + } 1098 + 1099 + f := sres.Files[0] 1100 + if f.Version != "v-master" { 1101 + t.Fatalf("got file %#v, want version 'v-master'", f) 1102 + } 691 1103 }) 692 - if len(sres.Files) != 1 { 693 - t.Fatalf("got %v, want 1 result from f2", sres.Files) 694 - } 1104 + 1105 + t.Run("ChunkMatches", func(t *testing.T) { 1106 + sres := searchForTest(t, b, &query.Substring{ 1107 + Pattern: "needle", 1108 + }, chunkOpts) 1109 + if len(sres.Files) != 1 { 1110 + t.Fatalf("got %v, want 1 result from f2", sres.Files) 1111 + } 695 1112 696 - f := sres.Files[0] 697 - if f.Version != "v-master" { 698 - t.Fatalf("got file %#v, want version 'v-master'", f) 699 - } 1113 + f := sres.Files[0] 1114 + if f.Version != "v-master" { 1115 + t.Fatalf("got file %#v, want version 'v-master'", f) 1116 + } 1117 + }) 700 1118 } 701 1119 702 1120 func mustParseRE(s string) *syntax.Regexp { ··· 718 1136 Content: content, 719 1137 }) 720 1138 721 - sres := searchForTest(t, b, 722 - &query.Regexp{ 723 - Regexp: mustParseRE("dle.*bla"), 724 - }) 1139 + t.Run("LineMatches", func(t *testing.T) { 1140 + sres := searchForTest(t, b, 1141 + &query.Regexp{ 1142 + Regexp: mustParseRE("dle.*bla"), 1143 + }) 725 1144 726 - if len(sres.Files) != 1 || len(sres.Files[0].LineMatches) != 1 { 727 - t.Fatalf("got %v, want 1 match in 1 file", sres.Files) 728 - } 1145 + if len(sres.Files) != 1 || len(sres.Files[0].LineMatches) != 1 { 1146 + t.Fatalf("got %v, want 1 match in 1 file", sres.Files) 1147 + } 729 1148 730 - got := sres.Files[0].LineMatches[0] 731 - want := LineMatch{ 732 - LineFragments: []LineFragmentMatch{{ 733 - LineOffset: 3, 734 - Offset: 3, 735 - MatchLength: 11, 736 - }}, 737 - Line: content, 738 - FileName: false, 739 - LineNumber: 1, 740 - LineStart: 0, 741 - LineEnd: 14, 742 - } 1149 + got := sres.Files[0].LineMatches[0] 1150 + want := LineMatch{ 1151 + LineFragments: []LineFragmentMatch{{ 1152 + LineOffset: 3, 1153 + Offset: 3, 1154 + MatchLength: 11, 1155 + }}, 1156 + Line: content, 1157 + FileName: false, 1158 + LineNumber: 1, 1159 + LineStart: 0, 1160 + LineEnd: 14, 1161 + } 743 1162 744 - if !reflect.DeepEqual(got, want) { 745 - t.Errorf("got %#v, want %#v", got, want) 746 - } 1163 + if !reflect.DeepEqual(got, want) { 1164 + t.Errorf("got %#v, want %#v", got, want) 1165 + } 1166 + }) 1167 + 1168 + t.Run("ChunkMatches", func(t *testing.T) { 1169 + sres := searchForTest(t, b, 1170 + &query.Regexp{ 1171 + Regexp: mustParseRE("dle.*bla"), 1172 + }, chunkOpts) 1173 + 1174 + if len(sres.Files) != 1 || len(sres.Files[0].ChunkMatches) != 1 { 1175 + t.Fatalf("got %v, want 1 match in 1 file", sres.Files) 1176 + } 1177 + 1178 + got := sres.Files[0].ChunkMatches[0] 1179 + want := ChunkMatch{ 1180 + Content: content, 1181 + ContentStart: Location{ByteOffset: 0, LineNumber: 1, Column: 1}, 1182 + Ranges: []Range{{ 1183 + Start: Location{ByteOffset: 3, LineNumber: 1, Column: 4}, 1184 + End: Location{ByteOffset: 14, LineNumber: 1, Column: 15}, 1185 + }}, 1186 + } 1187 + 1188 + if diff := cmp.Diff(want, got); diff != "" { 1189 + t.Fatal(diff) 1190 + } 1191 + }) 747 1192 } 748 1193 749 1194 func TestRegexpFile(t *testing.T) { ··· 754 1199 Document{Name: name, Content: content}, 755 1200 Document{Name: "play.txt", Content: content}) 756 1201 757 - sres := searchForTest(t, b, 758 - &query.Regexp{ 759 - Regexp: mustParseRE("play.*mussel"), 760 - FileName: true, 761 - }) 1202 + t.Run("LineMatches", func(t *testing.T) { 1203 + sres := searchForTest(t, b, 1204 + &query.Regexp{ 1205 + Regexp: mustParseRE("play.*mussel"), 1206 + FileName: true, 1207 + }) 1208 + 1209 + if len(sres.Files) != 1 || len(sres.Files[0].LineMatches) != 1 { 1210 + t.Fatalf("got %v, want 1 match in 1 file", sres.Files) 1211 + } 1212 + 1213 + if sres.Files[0].FileName != name { 1214 + t.Errorf("got match %#v, want name %q", sres.Files[0], name) 1215 + } 1216 + }) 762 1217 763 - if len(sres.Files) != 1 || len(sres.Files[0].LineMatches) != 1 { 764 - t.Fatalf("got %v, want 1 match in 1 file", sres.Files) 765 - } 1218 + t.Run("ChunkMatches", func(t *testing.T) { 1219 + sres := searchForTest(t, b, 1220 + &query.Regexp{ 1221 + Regexp: mustParseRE("play.*mussel"), 1222 + FileName: true, 1223 + }, chunkOpts) 766 1224 767 - if sres.Files[0].FileName != name { 768 - t.Errorf("got match %#v, want name %q", sres.Files[0], name) 769 - } 1225 + if len(sres.Files) != 1 || len(sres.Files[0].ChunkMatches) != 1 { 1226 + t.Fatalf("got %v, want 1 match in 1 file", sres.Files) 1227 + } 1228 + 1229 + if sres.Files[0].FileName != name { 1230 + t.Errorf("got match %#v, want name %q", sres.Files[0], name) 1231 + } 1232 + }) 770 1233 } 771 1234 772 1235 func TestRegexpOrder(t *testing.T) { ··· 776 1239 b := testIndexBuilder(t, nil, 777 1240 Document{Name: "f1", Content: content}) 778 1241 779 - sres := searchForTest(t, b, 780 - &query.Regexp{ 781 - Regexp: mustParseRE("dle.*bla"), 782 - }) 1242 + t.Run("LineMatches", func(t *testing.T) { 1243 + sres := searchForTest(t, b, 1244 + &query.Regexp{ 1245 + Regexp: mustParseRE("dle.*bla"), 1246 + }) 1247 + 1248 + if len(sres.Files) != 0 { 1249 + t.Fatalf("got %v, want 0 matches", sres.Files) 1250 + } 1251 + }) 1252 + 1253 + t.Run("ChunkMatches", func(t *testing.T) { 1254 + sres := searchForTest(t, b, 1255 + &query.Regexp{ 1256 + Regexp: mustParseRE("dle.*bla"), 1257 + }) 783 1258 784 - if len(sres.Files) != 0 { 785 - t.Fatalf("got %v, want 0 matches", sres.Files) 786 - } 1259 + if len(sres.Files) != 0 { 1260 + t.Fatalf("got %v, want 0 matches", sres.Files) 1261 + } 1262 + }) 787 1263 } 788 1264 789 1265 func TestRepoName(t *testing.T) { ··· 793 1269 b := testIndexBuilder(t, &Repository{Name: "bla"}, 794 1270 Document{Name: "f1", Content: content}) 795 1271 796 - sres := searchForTest(t, b, 797 - query.NewAnd( 798 - &query.Substring{Pattern: "needle"}, 799 - &query.Repo{Regexp: regexp.MustCompile("foo")}, 800 - )) 1272 + t.Run("LineMatches", func(t *testing.T) { 1273 + sres := searchForTest(t, b, 1274 + query.NewAnd( 1275 + &query.Substring{Pattern: "needle"}, 1276 + &query.Repo{Regexp: regexp.MustCompile("foo")}, 1277 + )) 1278 + 1279 + if len(sres.Files) != 0 { 1280 + t.Fatalf("got %v, want 0 matches", sres.Files) 1281 + } 1282 + 1283 + if sres.Stats.FilesConsidered > 0 { 1284 + t.Fatalf("got FilesConsidered %d, should have short circuited", sres.Stats.FilesConsidered) 1285 + } 1286 + 1287 + sres = searchForTest(t, b, 1288 + query.NewAnd( 1289 + &query.Substring{Pattern: "needle"}, 1290 + &query.Repo{Regexp: regexp.MustCompile("bla")}, 1291 + )) 1292 + if len(sres.Files) != 1 { 1293 + t.Fatalf("got %v, want 1 match", sres.Files) 1294 + } 1295 + }) 1296 + 1297 + t.Run("ChunkMatches", func(t *testing.T) { 1298 + sres := searchForTest(t, b, 1299 + query.NewAnd( 1300 + &query.Substring{Pattern: "needle"}, 1301 + &query.Repo{Regexp: regexp.MustCompile("foo")}, 1302 + ), 1303 + chunkOpts, 1304 + ) 801 1305 802 - if len(sres.Files) != 0 { 803 - t.Fatalf("got %v, want 0 matches", sres.Files) 804 - } 1306 + if len(sres.Files) != 0 { 1307 + t.Fatalf("got %v, want 0 matches", sres.Files) 1308 + } 805 1309 806 - if sres.Stats.FilesConsidered > 0 { 807 - t.Fatalf("got FilesConsidered %d, should have short circuited", sres.Stats.FilesConsidered) 808 - } 1310 + if sres.Stats.FilesConsidered > 0 { 1311 + t.Fatalf("got FilesConsidered %d, should have short circuited", sres.Stats.FilesConsidered) 1312 + } 809 1313 810 - sres = searchForTest(t, b, 811 - query.NewAnd( 812 - &query.Substring{Pattern: "needle"}, 813 - &query.Repo{Regexp: regexp.MustCompile("bla")}, 814 - )) 815 - if len(sres.Files) != 1 { 816 - t.Fatalf("got %v, want 1 match", sres.Files) 817 - } 1314 + sres = searchForTest(t, b, 1315 + query.NewAnd( 1316 + &query.Substring{Pattern: "needle"}, 1317 + &query.Repo{Regexp: regexp.MustCompile("bla")}, 1318 + )) 1319 + if len(sres.Files) != 1 { 1320 + t.Fatalf("got %v, want 1 match", sres.Files) 1321 + } 1322 + }) 818 1323 } 819 1324 820 1325 func TestMergeMatches(t *testing.T) { ··· 822 1327 b := testIndexBuilder(t, nil, 823 1328 Document{Name: "f1", Content: content}) 824 1329 825 - sres := searchForTest(t, b, 826 - &query.Substring{Pattern: "bla"}) 827 - if len(sres.Files) != 1 || len(sres.Files[0].LineMatches) != 1 { 828 - t.Fatalf("got %v, want 1 match", sres.Files) 829 - } 1330 + t.Run("LineMatches", func(t *testing.T) { 1331 + sres := searchForTest(t, b, 1332 + &query.Substring{Pattern: "bla"}) 1333 + if len(sres.Files) != 1 || len(sres.Files[0].LineMatches) != 1 { 1334 + t.Fatalf("got %v, want 1 match", sres.Files) 1335 + } 1336 + }) 1337 + 1338 + t.Run("ChunkMatches", func(t *testing.T) { 1339 + sres := searchForTest(t, b, 1340 + &query.Substring{Pattern: "bla"}, 1341 + chunkOpts, 1342 + ) 1343 + if len(sres.Files) != 1 || len(sres.Files[0].ChunkMatches) != 1 { 1344 + t.Fatalf("got %v, want 1 match", sres.Files) 1345 + } 1346 + }) 830 1347 } 831 1348 832 1349 func TestRepoURL(t *testing.T) { ··· 856 1373 Content: content, 857 1374 }) 858 1375 859 - res := searchForTest(t, b, 860 - &query.Regexp{ 861 - Regexp: mustParseRE("func.*Gitiles"), 862 - CaseSensitive: true, 863 - }) 1376 + t.Run("LineMatches", func(t *testing.T) { 1377 + res := searchForTest(t, b, 1378 + &query.Regexp{ 1379 + Regexp: mustParseRE("func.*Gitiles"), 1380 + CaseSensitive: true, 1381 + }) 1382 + 1383 + if len(res.Files) != 1 { 1384 + t.Fatalf("got %v, want one match", res.Files) 1385 + } 1386 + }) 1387 + 1388 + t.Run("ChunkMatches", func(t *testing.T) { 1389 + res := searchForTest(t, b, 1390 + &query.Regexp{ 1391 + Regexp: mustParseRE("func.*Gitiles"), 1392 + CaseSensitive: true, 1393 + }, 1394 + chunkOpts, 1395 + ) 864 1396 865 - if len(res.Files) != 1 { 866 - t.Fatalf("got %v, want one match", res.Files) 867 - } 1397 + if len(res.Files) != 1 { 1398 + t.Fatalf("got %v, want one match", res.Files) 1399 + } 1400 + }) 868 1401 } 869 1402 870 1403 func TestRegexpCaseFolding(t *testing.T) { ··· 887 1420 content := []byte("BLABLABLA") 888 1421 b := testIndexBuilder(t, nil, 889 1422 Document{Name: "f1", Content: content}) 890 - res := searchForTest(t, b, 891 - &query.Regexp{ 892 - Regexp: mustParseRE("[xb][xl][xa]"), 893 - CaseSensitive: true, 894 - }) 1423 + 1424 + t.Run("LineMatches", func(t *testing.T) { 1425 + res := searchForTest(t, b, 1426 + &query.Regexp{ 1427 + Regexp: mustParseRE("[xb][xl][xa]"), 1428 + CaseSensitive: true, 1429 + }) 1430 + 1431 + if len(res.Files) > 0 { 1432 + t.Fatalf("got %v, want no matches", res.Files) 1433 + } 1434 + }) 895 1435 896 - if len(res.Files) > 0 { 897 - t.Fatalf("got %v, want no matches", res.Files) 898 - } 1436 + t.Run("ChunkMatches", func(t *testing.T) { 1437 + res := searchForTest(t, b, 1438 + &query.Regexp{ 1439 + Regexp: mustParseRE("[xb][xl][xa]"), 1440 + CaseSensitive: true, 1441 + }, 1442 + chunkOpts, 1443 + ) 1444 + 1445 + if len(res.Files) > 0 { 1446 + t.Fatalf("got %v, want no matches", res.Files) 1447 + } 1448 + }) 899 1449 } 900 1450 901 1451 func TestNegativeRegexp(t *testing.T) { 902 1452 content := []byte("BLABLABLA needle bla") 903 1453 b := testIndexBuilder(t, nil, 904 1454 Document{Name: "f1", Content: content}) 905 - res := searchForTest(t, b, 906 - query.NewAnd( 907 - &query.Substring{ 908 - Pattern: "needle", 909 - }, 910 - &query.Not{ 911 - Child: &query.Regexp{ 912 - Regexp: mustParseRE(".cs"), 1455 + 1456 + t.Run("LineMatches", func(t *testing.T) { 1457 + res := searchForTest(t, b, 1458 + query.NewAnd( 1459 + &query.Substring{ 1460 + Pattern: "needle", 913 1461 }, 914 - })) 1462 + &query.Not{ 1463 + Child: &query.Regexp{ 1464 + Regexp: mustParseRE(".cs"), 1465 + }, 1466 + })) 915 1467 916 - if len(res.Files) != 1 { 917 - t.Fatalf("got %v, want 1 match", res.Files) 918 - } 1468 + if len(res.Files) != 1 { 1469 + t.Fatalf("got %v, want 1 match", res.Files) 1470 + } 1471 + }) 1472 + 1473 + t.Run("ChunkMatches", func(t *testing.T) { 1474 + res := searchForTest(t, b, 1475 + query.NewAnd( 1476 + &query.Substring{ 1477 + Pattern: "needle", 1478 + }, 1479 + &query.Not{ 1480 + Child: &query.Regexp{ 1481 + Regexp: mustParseRE(".cs"), 1482 + }, 1483 + }, 1484 + ), 1485 + chunkOpts) 1486 + 1487 + if len(res.Files) != 1 { 1488 + t.Fatalf("got %v, want 1 match", res.Files) 1489 + } 1490 + }) 919 1491 } 920 1492 921 1493 func TestSymbolRank(t *testing.T) { ··· 936 1508 Content: content, 937 1509 }) 938 1510 939 - res := searchForTest(t, b, 940 - &query.Substring{ 941 - CaseSensitive: false, 942 - Pattern: "bla", 943 - }) 1511 + t.Run("LineMatches", func(t *testing.T) { 1512 + res := searchForTest(t, b, 1513 + &query.Substring{ 1514 + CaseSensitive: false, 1515 + Pattern: "bla", 1516 + }) 1517 + 1518 + if len(res.Files) != 3 { 1519 + t.Fatalf("got %d files, want 3 files. Full data: %v", len(res.Files), res.Files) 1520 + } 1521 + if res.Files[0].FileName != "f2" { 1522 + t.Errorf("got %#v, want 'f2' as top match", res.Files[0]) 1523 + } 1524 + }) 944 1525 945 - if len(res.Files) != 3 { 946 - t.Fatalf("got %d files, want 3 files. Full data: %v", len(res.Files), res.Files) 947 - } 948 - if res.Files[0].FileName != "f2" { 949 - t.Errorf("got %#v, want 'f2' as top match", res.Files[0]) 950 - } 1526 + t.Run("ChunkMatches", func(t *testing.T) { 1527 + res := searchForTest(t, b, 1528 + &query.Substring{ 1529 + CaseSensitive: false, 1530 + Pattern: "bla", 1531 + }, chunkOpts) 1532 + 1533 + if len(res.Files) != 3 { 1534 + t.Fatalf("got %d files, want 3 files. Full data: %v", len(res.Files), res.Files) 1535 + } 1536 + if res.Files[0].FileName != "f2" { 1537 + t.Errorf("got %#v, want 'f2' as top match", res.Files[0]) 1538 + } 1539 + }) 951 1540 } 952 1541 953 1542 func TestSymbolRankRegexpUTF8(t *testing.T) { ··· 970 1559 Content: content, 971 1560 }) 972 1561 973 - res := searchForTest(t, b, 974 - &query.Regexp{ 975 - Regexp: mustParseRE("b.a"), 976 - }) 1562 + t.Run("LineMatches", func(t *testing.T) { 1563 + res := searchForTest(t, b, 1564 + &query.Regexp{ 1565 + Regexp: mustParseRE("b.a"), 1566 + }) 1567 + 1568 + if len(res.Files) != 3 { 1569 + t.Fatalf("got %#v, want 3 files", res.Files) 1570 + } 1571 + if res.Files[0].FileName != "f2" { 1572 + t.Errorf("got %#v, want 'f2' as top match", res.Files[0]) 1573 + } 1574 + }) 1575 + 1576 + t.Run("ChunjkMatches", func(t *testing.T) { 1577 + res := searchForTest(t, b, 1578 + &query.Regexp{ 1579 + Regexp: mustParseRE("b.a"), 1580 + }, chunkOpts) 977 1581 978 - if len(res.Files) != 3 { 979 - t.Fatalf("got %#v, want 3 files", res.Files) 980 - } 981 - if res.Files[0].FileName != "f2" { 982 - t.Errorf("got %#v, want 'f2' as top match", res.Files[0]) 983 - } 1582 + if len(res.Files) != 3 { 1583 + t.Fatalf("got %#v, want 3 files", res.Files) 1584 + } 1585 + if res.Files[0].FileName != "f2" { 1586 + t.Errorf("got %#v, want 'f2' as top match", res.Files[0]) 1587 + } 1588 + }) 984 1589 } 985 1590 986 1591 func TestPartialSymbolRank(t *testing.T) { ··· 1004 1609 Symbols: []DocumentSection{{4, 9}}, 1005 1610 }) 1006 1611 1007 - res := searchForTest(t, b, 1008 - &query.Substring{ 1009 - Pattern: "bla", 1010 - }) 1612 + t.Run("LineMatches", func(t *testing.T) { 1613 + res := searchForTest(t, b, 1614 + &query.Substring{ 1615 + Pattern: "bla", 1616 + }) 1617 + 1618 + if len(res.Files) != 3 { 1619 + t.Fatalf("got %#v, want 3 files", res.Files) 1620 + } 1621 + if res.Files[0].FileName != "f2" { 1622 + t.Errorf("got %#v, want 'f2' as top match", res.Files[0]) 1623 + } 1624 + }) 1625 + 1626 + t.Run("ChunkMatches", func(t *testing.T) { 1627 + res := searchForTest(t, b, 1628 + &query.Substring{ 1629 + Pattern: "bla", 1630 + }, chunkOpts) 1011 1631 1012 - if len(res.Files) != 3 { 1013 - t.Fatalf("got %#v, want 3 files", res.Files) 1014 - } 1015 - if res.Files[0].FileName != "f2" { 1016 - t.Errorf("got %#v, want 'f2' as top match", res.Files[0]) 1017 - } 1632 + if len(res.Files) != 3 { 1633 + t.Fatalf("got %#v, want 3 files", res.Files) 1634 + } 1635 + if res.Files[0].FileName != "f2" { 1636 + t.Errorf("got %#v, want 'f2' as top match", res.Files[0]) 1637 + } 1638 + }) 1018 1639 } 1019 1640 1020 1641 func TestNegativeRepo(t *testing.T) { ··· 1024 1645 Name: "bla", 1025 1646 }, Document{Name: "f1", Content: content}) 1026 1647 1027 - sres := searchForTest(t, b, 1028 - query.NewAnd( 1029 - &query.Substring{Pattern: "needle"}, 1030 - &query.Not{Child: &query.Repo{Regexp: regexp.MustCompile("bla")}}, 1031 - )) 1648 + t.Run("LineMatches", func(t *testing.T) { 1649 + sres := searchForTest(t, b, 1650 + query.NewAnd( 1651 + &query.Substring{Pattern: "needle"}, 1652 + &query.Not{Child: &query.Repo{Regexp: regexp.MustCompile("bla")}}, 1653 + )) 1654 + 1655 + if len(sres.Files) != 0 { 1656 + t.Fatalf("got %v, want 0 matches", sres.Files) 1657 + } 1658 + }) 1659 + 1660 + t.Run("ChunkMatches", func(t *testing.T) { 1661 + sres := searchForTest(t, b, 1662 + query.NewAnd( 1663 + &query.Substring{Pattern: "needle"}, 1664 + &query.Not{Child: &query.Repo{Regexp: regexp.MustCompile("bla")}}, 1665 + ), chunkOpts) 1032 1666 1033 - if len(sres.Files) != 0 { 1034 - t.Fatalf("got %v, want 0 matches", sres.Files) 1035 - } 1667 + if len(sres.Files) != 0 { 1668 + t.Fatalf("got %v, want 0 matches", sres.Files) 1669 + } 1670 + }) 1036 1671 } 1037 1672 1038 1673 func TestListRepos(t *testing.T) { ··· 1228 1863 b := testIndexBuilder(t, nil, 1229 1864 Document{Name: "f1", Content: []byte("needle")}, 1230 1865 Document{Name: "f2", Content: []byte("banana")}) 1231 - sres := searchForTest(t, b, query.NewOr( 1232 - &query.Substring{Pattern: "needle"}, 1233 - &query.Substring{Pattern: "banana"})) 1866 + t.Run("LineMatches", func(t *testing.T) { 1867 + sres := searchForTest(t, b, query.NewOr( 1868 + &query.Substring{Pattern: "needle"}, 1869 + &query.Substring{Pattern: "banana"})) 1870 + 1871 + if len(sres.Files) != 2 { 1872 + t.Fatalf("got %v, want 2 files", sres.Files) 1873 + } 1874 + }) 1875 + 1876 + t.Run("ChunkMatches", func(t *testing.T) { 1877 + sres := searchForTest(t, b, query.NewOr( 1878 + &query.Substring{Pattern: "needle"}, 1879 + &query.Substring{Pattern: "banana"})) 1234 1880 1235 - if len(sres.Files) != 2 { 1236 - t.Fatalf("got %v, want 2 files", sres.Files) 1237 - } 1881 + if len(sres.Files) != 2 { 1882 + t.Fatalf("got %v, want 2 files", sres.Files) 1883 + } 1884 + }) 1238 1885 } 1239 1886 1240 1887 func TestImportantCutoff(t *testing.T) { ··· 1251 1898 Name: "f2", 1252 1899 Content: content, 1253 1900 }) 1254 - opts := SearchOptions{ 1255 - ShardMaxImportantMatch: 1, 1256 - } 1257 1901 1258 - sres := searchForTest(t, b, &query.Substring{Pattern: "bla"}, opts) 1259 - if len(sres.Files) != 1 || sres.Files[0].FileName != "f1" { 1260 - t.Errorf("got %v, wanted 1 match 'f1'", sres.Files) 1261 - } 1902 + t.Run("LineMatches", func(t *testing.T) { 1903 + opts := SearchOptions{ 1904 + ShardMaxImportantMatch: 1, 1905 + } 1906 + sres := searchForTest(t, b, &query.Substring{Pattern: "bla"}, opts) 1907 + if len(sres.Files) != 1 || sres.Files[0].FileName != "f1" { 1908 + t.Errorf("got %v, wanted 1 match 'f1'", sres.Files) 1909 + } 1910 + }) 1911 + 1912 + t.Run("ChunkMatches", func(t *testing.T) { 1913 + opts := SearchOptions{ 1914 + ShardMaxImportantMatch: 1, 1915 + ChunkMatches: true, 1916 + } 1917 + sres := searchForTest(t, b, &query.Substring{Pattern: "bla"}, opts) 1918 + if len(sres.Files) != 1 || sres.Files[0].FileName != "f1" { 1919 + t.Errorf("got %v, wanted 1 match 'f1'", sres.Files) 1920 + } 1921 + }) 1262 1922 } 1263 1923 1264 1924 func TestFrequency(t *testing.T) { ··· 1270 1930 Content: content, 1271 1931 }) 1272 1932 1273 - sres := searchForTest(t, b, &query.Substring{Pattern: "slashdot"}) 1274 - if len(sres.Files) != 0 { 1275 - t.Errorf("got %v, wanted 0 matches", sres.Files) 1276 - } 1933 + t.Run("LineMatches", func(t *testing.T) { 1934 + sres := searchForTest(t, b, &query.Substring{Pattern: "slashdot"}) 1935 + if len(sres.Files) != 0 { 1936 + t.Errorf("got %v, wanted 0 matches", sres.Files) 1937 + } 1938 + }) 1939 + 1940 + t.Run("ChunkMatches", func(t *testing.T) { 1941 + sres := searchForTest(t, b, &query.Substring{Pattern: "slashdot"}, chunkOpts) 1942 + if len(sres.Files) != 0 { 1943 + t.Errorf("got %v, wanted 0 matches", sres.Files) 1944 + } 1945 + }) 1277 1946 } 1278 1947 1279 1948 func TestMatchNewline(t *testing.T) { ··· 1290 1959 Content: content, 1291 1960 }) 1292 1961 1293 - sres := searchForTest(t, b, &query.Regexp{Regexp: re, CaseSensitive: true}) 1294 - if len(sres.Files) != 1 { 1295 - t.Errorf("got %v, wanted 1 matches", sres.Files) 1296 - } else if l := sres.Files[0].LineMatches[0].Line; !bytes.Equal(l, content[len("pqr\n"):]) { 1297 - t.Errorf("got match line %q, want %q", l, content) 1298 - } 1962 + t.Run("LineMatches", func(t *testing.T) { 1963 + sres := searchForTest(t, b, &query.Regexp{Regexp: re, CaseSensitive: true}) 1964 + if len(sres.Files) != 1 { 1965 + t.Errorf("got %v, wanted 1 matches", sres.Files) 1966 + } else if l := sres.Files[0].LineMatches[0].Line; !bytes.Equal(l, content[len("pqr\n"):]) { 1967 + t.Errorf("got match line %q, want %q", l, content) 1968 + } 1969 + }) 1970 + 1971 + t.Run("ChunkMatches", func(t *testing.T) { 1972 + sres := searchForTest(t, b, &query.Regexp{Regexp: re, CaseSensitive: true}, chunkOpts) 1973 + if len(sres.Files) != 1 { 1974 + t.Errorf("got %v, wanted 1 matches", sres.Files) 1975 + } else if c := sres.Files[0].ChunkMatches[0].Content; !bytes.Equal(c, content) { 1976 + t.Errorf("got match line %q, want %q", c, content) 1977 + } 1978 + }) 1299 1979 } 1300 1980 1301 1981 func TestSubRepo(t *testing.T) { ··· 1336 2016 Document{Name: "f1", Content: []byte("bla needle bla")}, 1337 2017 Document{Name: "needle-file-branch", Content: []byte("bla content")}) 1338 2018 1339 - sres := searchForTest(t, b, &query.Substring{Pattern: "needle"}) 1340 - if len(sres.Files) != 2 { 1341 - t.Fatalf("got %v, wanted 2 matches", sres.Files) 1342 - } 2019 + t.Run("LineMatches", func(t *testing.T) { 2020 + sres := searchForTest(t, b, &query.Substring{Pattern: "needle"}) 2021 + if len(sres.Files) != 2 { 2022 + t.Fatalf("got %v, wanted 2 matches", sres.Files) 2023 + } 2024 + 2025 + sres = searchForTest(t, b, &query.Substring{Pattern: "needle", Content: true}) 2026 + if len(sres.Files) != 1 { 2027 + t.Fatalf("got %v, wanted 1 match", sres.Files) 2028 + } 1343 2029 1344 - sres = searchForTest(t, b, &query.Substring{Pattern: "needle", Content: true}) 1345 - if len(sres.Files) != 1 { 1346 - t.Fatalf("got %v, wanted 1 match", sres.Files) 1347 - } 2030 + if got, want := sres.Files[0].FileName, "f1"; got != want { 2031 + t.Errorf("got %q, want %q", got, want) 2032 + } 2033 + }) 1348 2034 1349 - if got, want := sres.Files[0].FileName, "f1"; got != want { 1350 - t.Errorf("got %q, want %q", got, want) 1351 - } 2035 + t.Run("ChunkMatches", func(t *testing.T) { 2036 + sres := searchForTest(t, b, &query.Substring{Pattern: "needle"}, chunkOpts) 2037 + if len(sres.Files) != 2 { 2038 + t.Fatalf("got %v, wanted 2 matches", sres.Files) 2039 + } 2040 + 2041 + sres = searchForTest(t, b, &query.Substring{Pattern: "needle", Content: true}, chunkOpts) 2042 + if len(sres.Files) != 1 { 2043 + t.Fatalf("got %v, wanted 1 match", sres.Files) 2044 + } 2045 + 2046 + if got, want := sres.Files[0].FileName, "f1"; got != want { 2047 + t.Errorf("got %q, want %q", got, want) 2048 + } 2049 + }) 1352 2050 } 1353 2051 1354 2052 func TestUnicodeExactMatch(t *testing.T) { ··· 1358 2056 b := testIndexBuilder(t, nil, 1359 2057 Document{Name: "f1", Content: content}) 1360 2058 1361 - if res := searchForTest(t, b, &query.Substring{Pattern: needle, CaseSensitive: true}); len(res.Files) != 1 { 1362 - t.Fatalf("case sensitive: got %v, wanted 1 match", res.Files) 1363 - } 2059 + t.Run("LineMatches", func(t *testing.T) { 2060 + if res := searchForTest(t, b, &query.Substring{Pattern: needle, CaseSensitive: true}); len(res.Files) != 1 { 2061 + t.Fatalf("case sensitive: got %v, wanted 1 match", res.Files) 2062 + } 2063 + }) 2064 + 2065 + t.Run("ChunkMatches", func(t *testing.T) { 2066 + res := searchForTest(t, b, &query.Substring{Pattern: needle, CaseSensitive: true}, chunkOpts) 2067 + if len(res.Files) != 1 { 2068 + t.Fatalf("case sensitive: got %v, wanted 1 match", res.Files) 2069 + } 2070 + }) 1364 2071 } 1365 2072 1366 2073 func TestUnicodeCoverContent(t *testing.T) { ··· 1370 2077 b := testIndexBuilder(t, nil, 1371 2078 Document{Name: "f1", Content: content}) 1372 2079 1373 - if res := searchForTest(t, b, &query.Substring{Pattern: "NÉÉDLÉ", CaseSensitive: true}); len(res.Files) != 0 { 1374 - t.Fatalf("case sensitive: got %v, wanted 0 match", res.Files) 1375 - } 2080 + t.Run("LineMatches", func(t *testing.T) { 2081 + if res := searchForTest(t, b, &query.Substring{Pattern: "NÉÉDLÉ", CaseSensitive: true}); len(res.Files) != 0 { 2082 + t.Fatalf("case sensitive: got %v, wanted 0 match", res.Files) 2083 + } 2084 + 2085 + res := searchForTest(t, b, &query.Substring{Pattern: "NÉÉDLÉ"}) 2086 + if len(res.Files) != 1 { 2087 + t.Fatalf("case insensitive: got %v, wanted 1 match", res.Files) 2088 + } 2089 + 2090 + if got, want := res.Files[0].LineMatches[0].LineFragments[0].Offset, uint32(strings.Index(string(content), needle)); got != want { 2091 + t.Errorf("got %d want %d", got, want) 2092 + } 2093 + }) 2094 + 2095 + t.Run("ChunkMatches", func(t *testing.T) { 2096 + res := searchForTest(t, b, &query.Substring{Pattern: "NÉÉDLÉ", CaseSensitive: true}, chunkOpts) 2097 + if len(res.Files) != 0 { 2098 + t.Fatalf("case sensitive: got %v, wanted 0 match", res.Files) 2099 + } 1376 2100 1377 - res := searchForTest(t, b, &query.Substring{Pattern: "NÉÉDLÉ"}) 1378 - if len(res.Files) != 1 { 1379 - t.Fatalf("case insensitive: got %v, wanted 1 match", res.Files) 1380 - } 2101 + res = searchForTest(t, b, &query.Substring{Pattern: "NÉÉDLÉ"}, chunkOpts) 2102 + if len(res.Files) != 1 { 2103 + t.Fatalf("case insensitive: got %v, wanted 1 match", res.Files) 2104 + } 1381 2105 1382 - if got, want := res.Files[0].LineMatches[0].LineFragments[0].Offset, uint32(strings.Index(string(content), needle)); got != want { 1383 - t.Errorf("got %d want %d", got, want) 1384 - } 2106 + got := res.Files[0].ChunkMatches[0].Ranges[0].Start.ByteOffset 2107 + want := uint32(strings.Index(string(content), needle)) 2108 + if got != want { 2109 + t.Errorf("got %d want %d", got, want) 2110 + } 2111 + }) 1385 2112 } 1386 2113 1387 2114 func TestUnicodeNonCoverContent(t *testing.T) { ··· 1391 2118 b := testIndexBuilder(t, nil, 1392 2119 Document{Name: "f1", Content: content}) 1393 2120 1394 - res := searchForTest(t, b, &query.Substring{Pattern: "NÉÉÁÁDLÉ", Content: true}) 1395 - if len(res.Files) != 1 { 1396 - t.Fatalf("got %v, wanted 1 match", res.Files) 1397 - } 2121 + t.Run("LineMatches", func(t *testing.T) { 2122 + res := searchForTest(t, b, &query.Substring{Pattern: "NÉÉÁÁDLÉ", Content: true}) 2123 + if len(res.Files) != 1 { 2124 + t.Fatalf("got %v, wanted 1 match", res.Files) 2125 + } 2126 + 2127 + if got, want := res.Files[0].LineMatches[0].LineFragments[0].Offset, uint32(strings.Index(string(content), needle)); got != want { 2128 + t.Errorf("got %d want %d", got, want) 2129 + } 2130 + }) 2131 + 2132 + t.Run("ChunkMatches", func(t *testing.T) { 2133 + res := searchForTest(t, b, &query.Substring{Pattern: "NÉÉÁÁDLÉ", Content: true}, chunkOpts) 2134 + if len(res.Files) != 1 { 2135 + t.Fatalf("got %v, wanted 1 match", res.Files) 2136 + } 1398 2137 1399 - if got, want := res.Files[0].LineMatches[0].LineFragments[0].Offset, uint32(strings.Index(string(content), needle)); got != want { 1400 - t.Errorf("got %d want %d", got, want) 1401 - } 2138 + got := res.Files[0].ChunkMatches[0].Ranges[0].Start.ByteOffset 2139 + want := uint32(strings.Index(string(content), needle)) 2140 + if got != want { 2141 + t.Errorf("got %d want %d", got, want) 2142 + } 2143 + }) 1402 2144 } 1403 2145 1404 2146 const kelvinCodePoint = 8490 ··· 1412 2154 " ee" + string([]rune{lower}) + "ee" + 1413 2155 " ee" + string([]rune{upper}) + "ee") 1414 2156 1415 - b := testIndexBuilder(t, nil, 1416 - Document{Name: "f1", Content: []byte(corpus)}) 2157 + t.Run("LineMatches", func(t *testing.T) { 2158 + b := testIndexBuilder(t, nil, 2159 + Document{Name: "f1", Content: []byte(corpus)}) 1417 2160 1418 - res := searchForTest(t, b, &query.Substring{Pattern: needle, Content: true}) 1419 - if len(res.Files) != 1 { 1420 - t.Fatalf("got %v, wanted 1 match", res.Files) 1421 - } 2161 + res := searchForTest(t, b, &query.Substring{Pattern: needle, Content: true}) 2162 + if len(res.Files) != 1 { 2163 + t.Fatalf("got %v, wanted 1 match", res.Files) 2164 + } 2165 + }) 2166 + 2167 + t.Run("ChunkMatches", func(t *testing.T) { 2168 + b := testIndexBuilder(t, nil, 2169 + Document{Name: "f1", Content: []byte(corpus)}) 2170 + 2171 + res := searchForTest(t, b, &query.Substring{Pattern: needle, Content: true}, chunkOpts) 2172 + if len(res.Files) != 1 { 2173 + t.Fatalf("got %v, wanted 1 match", res.Files) 2174 + } 2175 + }) 1422 2176 } 1423 2177 1424 2178 func TestUnicodeFileStartOffsets(t *testing.T) { ··· 1457 2211 Content: content, 1458 2212 }) 1459 2213 1460 - q := &query.Substring{Pattern: needle, Content: true} 1461 - res := searchForTest(t, b, q) 1462 - if len(res.Files) != 1 { 1463 - t.Errorf("got %v, want 1 result", res) 1464 - } 2214 + t.Run("LineMatches", func(t *testing.T) { 2215 + q := &query.Substring{Pattern: needle, Content: true} 2216 + res := searchForTest(t, b, q) 2217 + if len(res.Files) != 1 { 2218 + t.Errorf("got %v, want 1 result", res) 2219 + } 2220 + }) 2221 + 2222 + t.Run("ChunkMatches", func(t *testing.T) { 2223 + q := &query.Substring{Pattern: needle, Content: true} 2224 + res := searchForTest(t, b, q, chunkOpts) 2225 + if len(res.Files) != 1 { 2226 + t.Errorf("got %v, want 1 result", res) 2227 + } 2228 + }) 1465 2229 } 1466 2230 1467 2231 func TestEstimateDocCount(t *testing.T) { ··· 1471 2235 Document{Name: "f2", Content: content}, 1472 2236 ) 1473 2237 1474 - if sres := searchForTest(t, b, 1475 - query.NewAnd( 1476 - &query.Substring{Pattern: "needle"}, 1477 - &query.Repo{Regexp: regexp.MustCompile("reponame")}, 1478 - ), SearchOptions{ 1479 - EstimateDocCount: true, 1480 - }); sres.Stats.ShardFilesConsidered != 2 { 1481 - t.Errorf("got FilesConsidered = %d, want 2", sres.Stats.FilesConsidered) 1482 - } 1483 - if sres := searchForTest(t, b, 1484 - query.NewAnd( 1485 - &query.Substring{Pattern: "needle"}, 1486 - &query.Repo{Regexp: regexp.MustCompile("nomatch")}, 1487 - ), SearchOptions{ 1488 - EstimateDocCount: true, 1489 - }); sres.Stats.ShardFilesConsidered != 0 { 1490 - t.Errorf("got FilesConsidered = %d, want 0", sres.Stats.FilesConsidered) 1491 - } 2238 + t.Run("LineMatches", func(t *testing.T) { 2239 + if sres := searchForTest(t, b, 2240 + query.NewAnd( 2241 + &query.Substring{Pattern: "needle"}, 2242 + &query.Repo{Regexp: regexp.MustCompile("reponame")}, 2243 + ), SearchOptions{ 2244 + EstimateDocCount: true, 2245 + }); sres.Stats.ShardFilesConsidered != 2 { 2246 + t.Errorf("got FilesConsidered = %d, want 2", sres.Stats.FilesConsidered) 2247 + } 2248 + if sres := searchForTest(t, b, 2249 + query.NewAnd( 2250 + &query.Substring{Pattern: "needle"}, 2251 + &query.Repo{Regexp: regexp.MustCompile("nomatch")}, 2252 + ), SearchOptions{ 2253 + EstimateDocCount: true, 2254 + }); sres.Stats.ShardFilesConsidered != 0 { 2255 + t.Errorf("got FilesConsidered = %d, want 0", sres.Stats.FilesConsidered) 2256 + } 2257 + }) 2258 + 2259 + t.Run("ChunkMatches", func(t *testing.T) { 2260 + if sres := searchForTest(t, b, 2261 + query.NewAnd( 2262 + &query.Substring{Pattern: "needle"}, 2263 + &query.Repo{Regexp: regexp.MustCompile("reponame")}, 2264 + ), SearchOptions{ 2265 + EstimateDocCount: true, 2266 + ChunkMatches: true, 2267 + }); sres.Stats.ShardFilesConsidered != 2 { 2268 + t.Errorf("got FilesConsidered = %d, want 2", sres.Stats.FilesConsidered) 2269 + } 2270 + if sres := searchForTest(t, b, 2271 + query.NewAnd( 2272 + &query.Substring{Pattern: "needle"}, 2273 + &query.Repo{Regexp: regexp.MustCompile("nomatch")}, 2274 + ), SearchOptions{ 2275 + EstimateDocCount: true, 2276 + ChunkMatches: true, 2277 + }); sres.Stats.ShardFilesConsidered != 0 { 2278 + t.Errorf("got FilesConsidered = %d, want 0", sres.Stats.FilesConsidered) 2279 + } 2280 + }) 1492 2281 } 1493 2282 1494 2283 func TestUTF8CorrectCorpus(t *testing.T) { ··· 1506 2295 Content: []byte("hello"), 1507 2296 }) 1508 2297 1509 - q := &query.Substring{Pattern: needle, FileName: true} 1510 - res := searchForTest(t, b, q) 1511 - if len(res.Files) != 1 { 1512 - t.Errorf("got %v, want 1 result", res) 1513 - } 2298 + t.Run("LineMatches", func(t *testing.T) { 2299 + q := &query.Substring{Pattern: needle, FileName: true} 2300 + res := searchForTest(t, b, q) 2301 + if len(res.Files) != 1 { 2302 + t.Errorf("got %v, want 1 result", res) 2303 + } 2304 + }) 2305 + 2306 + t.Run("ChunkMatches", func(t *testing.T) { 2307 + q := &query.Substring{Pattern: needle, FileName: true} 2308 + res := searchForTest(t, b, q, chunkOpts) 2309 + if len(res.Files) != 1 { 2310 + t.Errorf("got %v, want 1 result", res) 2311 + } 2312 + }) 1514 2313 } 1515 2314 1516 2315 func TestBuilderStats(t *testing.T) { ··· 1536 2335 Content: []byte(strings.Repeat("abcd", 1024)), 1537 2336 }) 1538 2337 1539 - q := &query.Substring{Pattern: "abc", CaseSensitive: true, Content: true} 1540 - res := searchForTest(t, b, q) 2338 + t.Run("LineMatches", func(t *testing.T) { 2339 + q := &query.Substring{Pattern: "abc", CaseSensitive: true, Content: true} 2340 + res := searchForTest(t, b, q) 2341 + 2342 + // 4096 (content) + 2 (overhead: newlines or doc sections) 2343 + if got, want := res.Stats.ContentBytesLoaded, int64(4098); got != want { 2344 + t.Errorf("got content I/O %d, want %d", got, want) 2345 + } 2346 + 2347 + // 1024 entries, each 4 bytes apart. 4 fits into single byte 2348 + // delta encoded. 2349 + if got, want := res.Stats.IndexBytesLoaded, int64(1024); got != want { 2350 + t.Errorf("got index I/O %d, want %d", got, want) 2351 + } 2352 + }) 2353 + 2354 + t.Run("ChunkMatches", func(t *testing.T) { 2355 + q := &query.Substring{Pattern: "abc", CaseSensitive: true, Content: true} 2356 + res := searchForTest(t, b, q, chunkOpts) 1541 2357 1542 - // 4096 (content) + 2 (overhead: newlines or doc sections) 1543 - if got, want := res.Stats.ContentBytesLoaded, int64(4098); got != want { 1544 - t.Errorf("got content I/O %d, want %d", got, want) 1545 - } 2358 + // 4096 (content) + 2 (overhead: newlines or doc sections) 2359 + if got, want := res.Stats.ContentBytesLoaded, int64(4098); got != want { 2360 + t.Errorf("got content I/O %d, want %d", got, want) 2361 + } 1546 2362 1547 - // 1024 entries, each 4 bytes apart. 4 fits into single byte 1548 - // delta encoded. 1549 - if got, want := res.Stats.IndexBytesLoaded, int64(1024); got != want { 1550 - t.Errorf("got index I/O %d, want %d", got, want) 1551 - } 2363 + // 1024 entries, each 4 bytes apart. 4 fits into single byte 2364 + // delta encoded. 2365 + if got, want := res.Stats.IndexBytesLoaded, int64(1024); got != want { 2366 + t.Errorf("got index I/O %d, want %d", got, want) 2367 + } 2368 + }) 1552 2369 } 1553 2370 1554 2371 func TestStartLineAnchor(t *testing.T) { ··· 1561 2378 `), 1562 2379 }) 1563 2380 1564 - q, err := query.Parse("^start") 1565 - if err != nil { 1566 - t.Errorf("parse: %v", err) 1567 - } 2381 + t.Run("LineMatches", func(t *testing.T) { 2382 + q, err := query.Parse("^start") 2383 + if err != nil { 2384 + t.Errorf("parse: %v", err) 2385 + } 1568 2386 1569 - res := searchForTest(t, b, q) 1570 - if len(res.Files) != 1 { 1571 - t.Errorf("got %v, want 1 file", res.Files) 1572 - } 2387 + res := searchForTest(t, b, q) 2388 + if len(res.Files) != 1 { 2389 + t.Errorf("got %v, want 1 file", res.Files) 2390 + } 2391 + 2392 + q, err = query.Parse("^middle") 2393 + if err != nil { 2394 + t.Errorf("parse: %v", err) 2395 + } 2396 + res = searchForTest(t, b, q) 2397 + if len(res.Files) != 0 { 2398 + t.Errorf("got %v, want 0 files", res.Files) 2399 + } 2400 + }) 2401 + 2402 + t.Run("ChunkMatches", func(t *testing.T) { 2403 + q, err := query.Parse("^start") 2404 + if err != nil { 2405 + t.Errorf("parse: %v", err) 2406 + } 1573 2407 1574 - q, err = query.Parse("^middle") 1575 - if err != nil { 1576 - t.Errorf("parse: %v", err) 1577 - } 1578 - res = searchForTest(t, b, q) 1579 - if len(res.Files) != 0 { 1580 - t.Errorf("got %v, want 0 files", res.Files) 1581 - } 2408 + res := searchForTest(t, b, q, chunkOpts) 2409 + if len(res.Files) != 1 { 2410 + t.Errorf("got %v, want 1 file", res.Files) 2411 + } 2412 + 2413 + q, err = query.Parse("^middle") 2414 + if err != nil { 2415 + t.Errorf("parse: %v", err) 2416 + } 2417 + res = searchForTest(t, b, q, chunkOpts) 2418 + if len(res.Files) != 0 { 2419 + t.Errorf("got %v, want 0 files", res.Files) 2420 + } 2421 + }) 1582 2422 } 1583 2423 1584 2424 func TestAndOrUnicode(t *testing.T) { ··· 1600 2440 Branches: []string{"master"}, 1601 2441 }) 1602 2442 1603 - res := searchForTest(t, b, finalQ) 1604 - if len(res.Files) != 1 { 1605 - t.Errorf("got %v, want 1 result", res.Files) 1606 - } 2443 + t.Run("LineMatches", func(t *testing.T) { 2444 + res := searchForTest(t, b, finalQ) 2445 + if len(res.Files) != 1 { 2446 + t.Errorf("got %v, want 1 result", res.Files) 2447 + } 2448 + }) 2449 + 2450 + t.Run("ChunkMatches", func(t *testing.T) { 2451 + res := searchForTest(t, b, finalQ, chunkOpts) 2452 + if len(res.Files) != 1 { 2453 + t.Errorf("got %v, want 1 result", res.Files) 2454 + } 2455 + }) 1607 2456 } 1608 2457 1609 2458 func TestAndShort(t *testing.T) { ··· 1617 2466 q := query.NewAnd(&query.Substring{Pattern: "at"}, 1618 2467 &query.Substring{Pattern: "orange"}) 1619 2468 1620 - res := searchForTest(t, b, q) 1621 - if len(res.Files) != 1 || res.Files[0].FileName != "f1" { 1622 - t.Errorf("got %v, want 1 result", res.Files) 1623 - } 2469 + t.Run("LineMatches", func(t *testing.T) { 2470 + res := searchForTest(t, b, q) 2471 + if len(res.Files) != 1 || res.Files[0].FileName != "f1" { 2472 + t.Errorf("got %v, want 1 result", res.Files) 2473 + } 2474 + }) 2475 + 2476 + t.Run("ChunkMatches", func(t *testing.T) { 2477 + res := searchForTest(t, b, q, chunkOpts) 2478 + if len(res.Files) != 1 || res.Files[0].FileName != "f1" { 2479 + t.Errorf("got %v, want 1 result", res.Files) 2480 + } 2481 + }) 1624 2482 } 1625 2483 1626 2484 func TestNoCollectRegexpSubstring(t *testing.T) { ··· 1633 2491 Regexp: mustParseRE("final[,.]"), 1634 2492 } 1635 2493 1636 - res := searchForTest(t, b, q) 1637 - if len(res.Files) != 1 { 1638 - t.Fatalf("got %v, want 1 result", res.Files) 1639 - } 1640 - if f := res.Files[0]; len(f.LineMatches) != 1 { 1641 - t.Fatalf("got line matches %v, want 1 line match", printLineMatches(f.LineMatches)) 1642 - } 2494 + t.Run("LineMatches", func(t *testing.T) { 2495 + res := searchForTest(t, b, q) 2496 + if len(res.Files) != 1 { 2497 + t.Fatalf("got %v, want 1 result", res.Files) 2498 + } 2499 + if f := res.Files[0]; len(f.LineMatches) != 1 { 2500 + t.Fatalf("got line matches %v, want 1 line match", printLineMatches(f.LineMatches)) 2501 + } 2502 + }) 2503 + 2504 + t.Run("ChunkMatches", func(t *testing.T) { 2505 + res := searchForTest(t, b, q, chunkOpts) 2506 + if len(res.Files) != 1 { 2507 + t.Fatalf("got %v, want 1 result", res.Files) 2508 + } 2509 + if f := res.Files[0]; len(f.ChunkMatches) != 1 { 2510 + t.Fatalf("got line matches %v, want 1 line match", printLineMatches(f.LineMatches)) 2511 + } 2512 + }) 1643 2513 } 1644 2514 1645 2515 func printLineMatches(ms []LineMatch) string { ··· 1662 2532 q := query.NewAnd(&query.Substring{Pattern: "needle"}, 1663 2533 &query.Language{Language: "cpp"}) 1664 2534 1665 - res := searchForTest(t, b, q) 1666 - if len(res.Files) != 1 { 1667 - t.Fatalf("got %v, want 1 result in f3", res.Files) 1668 - } 1669 - f := res.Files[0] 1670 - if f.FileName != "f3" || f.Language != "cpp" { 1671 - t.Fatalf("got %v, want 1 match with language cpp", f) 1672 - } 2535 + t.Run("LineMatches", func(t *testing.T) { 2536 + res := searchForTest(t, b, q) 2537 + if len(res.Files) != 1 { 2538 + t.Fatalf("got %v, want 1 result in f3", res.Files) 2539 + } 2540 + f := res.Files[0] 2541 + if f.FileName != "f3" || f.Language != "cpp" { 2542 + t.Fatalf("got %v, want 1 match with language cpp", f) 2543 + } 2544 + }) 2545 + 2546 + t.Run("ChunkMatches", func(t *testing.T) { 2547 + res := searchForTest(t, b, q, chunkOpts) 2548 + if len(res.Files) != 1 { 2549 + t.Fatalf("got %v, want 1 result in f3", res.Files) 2550 + } 2551 + f := res.Files[0] 2552 + if f.FileName != "f3" || f.Language != "cpp" { 2553 + t.Fatalf("got %v, want 1 match with language cpp", f) 2554 + } 2555 + }) 1673 2556 } 1674 2557 1675 2558 func TestLangShortcut(t *testing.T) { ··· 1682 2565 q := query.NewAnd(&query.Substring{Pattern: "needle"}, 1683 2566 &query.Language{Language: "fortran"}) 1684 2567 1685 - res := searchForTest(t, b, q) 1686 - if len(res.Files) != 0 { 1687 - t.Fatalf("got %v, want 0 results", res.Files) 1688 - } 1689 - if res.Stats.IndexBytesLoaded > 0 { 1690 - t.Errorf("got IndexBytesLoaded %d, want 0", res.Stats.IndexBytesLoaded) 1691 - } 2568 + t.Run("LineMatches", func(t *testing.T) { 2569 + res := searchForTest(t, b, q) 2570 + if len(res.Files) != 0 { 2571 + t.Fatalf("got %v, want 0 results", res.Files) 2572 + } 2573 + if res.Stats.IndexBytesLoaded > 0 { 2574 + t.Errorf("got IndexBytesLoaded %d, want 0", res.Stats.IndexBytesLoaded) 2575 + } 2576 + }) 2577 + 2578 + t.Run("ChunkMatches", func(t *testing.T) { 2579 + res := searchForTest(t, b, q, chunkOpts) 2580 + if len(res.Files) != 0 { 2581 + t.Fatalf("got %v, want 0 results", res.Files) 2582 + } 2583 + if res.Stats.IndexBytesLoaded > 0 { 2584 + t.Errorf("got IndexBytesLoaded %d, want 0", res.Stats.IndexBytesLoaded) 2585 + } 2586 + }) 1692 2587 } 1693 2588 1694 2589 func TestNoTextMatchAtoms(t *testing.T) { ··· 1699 2594 Document{Name: "f3", Language: "cpp", Content: content}, 1700 2595 ) 1701 2596 q := query.NewAnd(&query.Language{Language: "java"}) 1702 - res := searchForTest(t, b, q) 1703 - if len(res.Files) != 1 { 1704 - t.Fatalf("got %v, want 1 result in f3", res.Files) 1705 - } 2597 + t.Run("LineMatches", func(t *testing.T) { 2598 + res := searchForTest(t, b, q) 2599 + if len(res.Files) != 1 { 2600 + t.Fatalf("got %v, want 1 result in f3", res.Files) 2601 + } 2602 + }) 2603 + 2604 + t.Run("ChunkMatches", func(t *testing.T) { 2605 + res := searchForTest(t, b, q, chunkOpts) 2606 + if len(res.Files) != 1 { 2607 + t.Fatalf("got %v, want 1 result in f3", res.Files) 2608 + } 2609 + }) 1706 2610 } 1707 2611 1708 2612 func TestNoPositiveAtoms(t *testing.T) { ··· 1715 2619 q := query.NewAnd( 1716 2620 &query.Not{Child: &query.Substring{Pattern: "xyz"}}, 1717 2621 &query.Repo{Regexp: regexp.MustCompile("reponame")}) 1718 - res := searchForTest(t, b, q) 1719 - if len(res.Files) != 2 { 1720 - t.Fatalf("got %v, want 2 results in f3", res.Files) 1721 - } 2622 + t.Run("LineMatches", func(t *testing.T) { 2623 + res := searchForTest(t, b, q) 2624 + if len(res.Files) != 2 { 2625 + t.Fatalf("got %v, want 2 results in f3", res.Files) 2626 + } 2627 + }) 2628 + t.Run("ChunkMatches", func(t *testing.T) { 2629 + res := searchForTest(t, b, q, chunkOpts) 2630 + if len(res.Files) != 2 { 2631 + t.Fatalf("got %v, want 2 results in f3", res.Files) 2632 + } 2633 + }) 1722 2634 } 1723 2635 1724 2636 func TestSymbolBoundaryStart(t *testing.T) { ··· 1735 2647 q := &query.Symbol{ 1736 2648 Expr: &query.Substring{Pattern: "start"}, 1737 2649 } 1738 - res := searchForTest(t, b, q) 1739 - if len(res.Files) != 1 || len(res.Files[0].LineMatches) != 1 { 1740 - t.Fatalf("got %v, want 1 line in 1 file", res.Files) 1741 - } 1742 - m := res.Files[0].LineMatches[0].LineFragments[0] 1743 - if m.Offset != 0 { 1744 - t.Fatalf("got offset %d want 0", m.Offset) 1745 - } 2650 + t.Run("LineMatches", func(t *testing.T) { 2651 + res := searchForTest(t, b, q) 2652 + if len(res.Files) != 1 || len(res.Files[0].LineMatches) != 1 { 2653 + t.Fatalf("got %v, want 1 line in 1 file", res.Files) 2654 + } 2655 + m := res.Files[0].LineMatches[0].LineFragments[0] 2656 + if m.Offset != 0 { 2657 + t.Fatalf("got offset %d want 0", m.Offset) 2658 + } 2659 + }) 2660 + 2661 + t.Run("ChunkMatches", func(t *testing.T) { 2662 + res := searchForTest(t, b, q, chunkOpts) 2663 + if len(res.Files) != 1 || len(res.Files[0].ChunkMatches) != 1 { 2664 + t.Fatalf("got %v, want 1 line in 1 file", res.Files) 2665 + } 2666 + m := res.Files[0].ChunkMatches[0].Ranges[0] 2667 + if m.Start.ByteOffset != 0 { 2668 + t.Fatalf("got offset %d want 0", m.Start.ByteOffset) 2669 + } 2670 + }) 1746 2671 } 1747 2672 1748 2673 func TestSymbolBoundaryEnd(t *testing.T) { ··· 1759 2684 q := &query.Symbol{ 1760 2685 Expr: &query.Substring{Pattern: "end"}, 1761 2686 } 1762 - res := searchForTest(t, b, q) 1763 - if len(res.Files) != 1 || len(res.Files[0].LineMatches) != 1 { 1764 - t.Fatalf("got %v, want 1 line in 1 file", res.Files) 1765 - } 1766 - m := res.Files[0].LineMatches[0].LineFragments[0] 1767 - if m.Offset != 14 { 1768 - t.Fatalf("got offset %d want 0", m.Offset) 1769 - } 2687 + t.Run("LineMatches", func(t *testing.T) { 2688 + res := searchForTest(t, b, q) 2689 + if len(res.Files) != 1 || len(res.Files[0].LineMatches) != 1 { 2690 + t.Fatalf("got %v, want 1 line in 1 file", res.Files) 2691 + } 2692 + m := res.Files[0].LineMatches[0].LineFragments[0] 2693 + if m.Offset != 14 { 2694 + t.Fatalf("got offset %d want 0", m.Offset) 2695 + } 2696 + }) 2697 + 2698 + t.Run("ChunkMatches", func(t *testing.T) { 2699 + res := searchForTest(t, b, q, chunkOpts) 2700 + if len(res.Files) != 1 || len(res.Files[0].ChunkMatches) != 1 { 2701 + t.Fatalf("got %v, want 1 line in 1 file", res.Files) 2702 + } 2703 + m := res.Files[0].ChunkMatches[0].Ranges[0] 2704 + if m.Start.ByteOffset != 14 { 2705 + t.Fatalf("got offset %d want 0", m.Start.ByteOffset) 2706 + } 2707 + }) 1770 2708 } 1771 2709 1772 2710 func TestSymbolSubstring(t *testing.T) { ··· 1783 2721 q := &query.Symbol{ 1784 2722 Expr: &query.Substring{Pattern: "bla"}, 1785 2723 } 1786 - res := searchForTest(t, b, q) 1787 - if len(res.Files) != 1 || len(res.Files[0].LineMatches) != 1 { 1788 - t.Fatalf("got %v, want 1 line in 1 file", res.Files) 1789 - } 1790 - m := res.Files[0].LineMatches[0].LineFragments[0] 1791 - if m.Offset != 7 || m.MatchLength != 3 { 1792 - t.Fatalf("got offset %d, size %d want 7 size 3", m.Offset, m.MatchLength) 1793 - } 2724 + t.Run("LineMatches", func(t *testing.T) { 2725 + res := searchForTest(t, b, q) 2726 + if len(res.Files) != 1 || len(res.Files[0].LineMatches) != 1 { 2727 + t.Fatalf("got %v, want 1 line in 1 file", res.Files) 2728 + } 2729 + m := res.Files[0].LineMatches[0].LineFragments[0] 2730 + if m.Offset != 7 || m.MatchLength != 3 { 2731 + t.Fatalf("got offset %d, size %d want 7 size 3", m.Offset, m.MatchLength) 2732 + } 2733 + }) 2734 + 2735 + t.Run("ChunkMatches", func(t *testing.T) { 2736 + res := searchForTest(t, b, q, chunkOpts) 2737 + if len(res.Files) != 1 || len(res.Files[0].ChunkMatches) != 1 { 2738 + t.Fatalf("got %v, want 1 line in 1 file", res.Files) 2739 + } 2740 + m := res.Files[0].ChunkMatches[0].Ranges[0] 2741 + if m.Start.ByteOffset != 7 || m.End.ByteOffset != 10 { 2742 + t.Fatalf("got offset %d, end %d want 7, 10", m.Start.ByteOffset, m.End.ByteOffset) 2743 + } 2744 + }) 1794 2745 } 1795 2746 1796 2747 func TestSymbolSubstringExact(t *testing.T) { ··· 1807 2758 q := &query.Symbol{ 1808 2759 Expr: &query.Substring{Pattern: "sym"}, 1809 2760 } 1810 - res := searchForTest(t, b, q) 1811 - if len(res.Files) != 1 || len(res.Files[0].LineMatches) != 1 { 1812 - t.Fatalf("got %v, want 1 line in 1 file", res.Files) 1813 - } 1814 - m := res.Files[0].LineMatches[0].LineFragments[0] 1815 - if m.Offset != 4 { 1816 - t.Fatalf("got offset %d, want 7", m.Offset) 1817 - } 2761 + t.Run("LineMatches", func(t *testing.T) { 2762 + res := searchForTest(t, b, q) 2763 + if len(res.Files) != 1 || len(res.Files[0].LineMatches) != 1 { 2764 + t.Fatalf("got %v, want 1 line in 1 file", res.Files) 2765 + } 2766 + m := res.Files[0].LineMatches[0].LineFragments[0] 2767 + if m.Offset != 4 { 2768 + t.Fatalf("got offset %d, want 7", m.Offset) 2769 + } 2770 + }) 2771 + 2772 + t.Run("ChunkMatches", func(t *testing.T) { 2773 + res := searchForTest(t, b, q, chunkOpts) 2774 + if len(res.Files) != 1 || len(res.Files[0].ChunkMatches) != 1 { 2775 + t.Fatalf("got %v, want 1 line in 1 file", res.Files) 2776 + } 2777 + m := res.Files[0].ChunkMatches[0].Ranges[0] 2778 + if m.Start.ByteOffset != 4 { 2779 + t.Fatalf("got offset %d, want 7", m.Start.ByteOffset) 2780 + } 2781 + }) 1818 2782 } 1819 2783 1820 2784 func TestSymbolRegexpExact(t *testing.T) { ··· 1831 2795 q := &query.Symbol{ 1832 2796 Expr: &query.Regexp{Regexp: mustParseRE("^bla$")}, 1833 2797 } 1834 - res := searchForTest(t, b, q) 1835 - if len(res.Files) != 1 || len(res.Files[0].LineMatches) != 1 { 1836 - t.Fatalf("got %v, want 1 line in 1 file", res.Files) 1837 - } 1838 - m := res.Files[0].LineMatches[0].LineFragments[0] 1839 - if m.Offset != 5 { 1840 - t.Fatalf("got offset %d, want 5", m.Offset) 1841 - } 2798 + t.Run("LineMatches", func(t *testing.T) { 2799 + res := searchForTest(t, b, q) 2800 + if len(res.Files) != 1 || len(res.Files[0].LineMatches) != 1 { 2801 + t.Fatalf("got %v, want 1 line in 1 file", res.Files) 2802 + } 2803 + m := res.Files[0].LineMatches[0].LineFragments[0] 2804 + if m.Offset != 5 { 2805 + t.Fatalf("got offset %d, want 5", m.Offset) 2806 + } 2807 + }) 2808 + 2809 + t.Run("ChunkMatches", func(t *testing.T) { 2810 + res := searchForTest(t, b, q, chunkOpts) 2811 + if len(res.Files) != 1 || len(res.Files[0].ChunkMatches) != 1 { 2812 + t.Fatalf("got %v, want 1 line in 1 file", res.Files) 2813 + } 2814 + m := res.Files[0].ChunkMatches[0].Ranges[0] 2815 + if m.Start.ByteOffset != 5 { 2816 + t.Fatalf("got offset %d, want 5", m.Start.ByteOffset) 2817 + } 2818 + }) 1842 2819 } 1843 2820 1844 2821 func TestSymbolRegexpPartial(t *testing.T) { ··· 1855 2832 q := &query.Symbol{ 1856 2833 Expr: &query.Regexp{Regexp: mustParseRE("(b|d)c(d|b)")}, 1857 2834 } 1858 - res := searchForTest(t, b, q) 1859 - if len(res.Files) != 1 || len(res.Files[0].LineMatches) != 1 { 1860 - t.Fatalf("got %v, want 1 line in 1 file", res.Files) 1861 - } 1862 - m := res.Files[0].LineMatches[0].LineFragments[0] 1863 - if m.Offset != 1 { 1864 - t.Fatalf("got offset %d, want 1", m.Offset) 1865 - } 1866 - if m.MatchLength != 3 { 1867 - t.Fatalf("got match length %d, want 3", m.MatchLength) 1868 - } 2835 + t.Run("LineMatches", func(t *testing.T) { 2836 + res := searchForTest(t, b, q) 2837 + if len(res.Files) != 1 || len(res.Files[0].LineMatches) != 1 { 2838 + t.Fatalf("got %v, want 1 line in 1 file", res.Files) 2839 + } 2840 + m := res.Files[0].LineMatches[0].LineFragments[0] 2841 + if m.Offset != 1 { 2842 + t.Fatalf("got offset %d, want 1", m.Offset) 2843 + } 2844 + if m.MatchLength != 3 { 2845 + t.Fatalf("got match length %d, want 3", m.MatchLength) 2846 + } 2847 + }) 2848 + 2849 + t.Run("ChunkMatches", func(t *testing.T) { 2850 + res := searchForTest(t, b, q, chunkOpts) 2851 + if len(res.Files) != 1 || len(res.Files[0].ChunkMatches) != 1 { 2852 + t.Fatalf("got %v, want 1 line in 1 file", res.Files) 2853 + } 2854 + m := res.Files[0].ChunkMatches[0].Ranges[0] 2855 + if m.Start.ByteOffset != 1 { 2856 + t.Fatalf("got offset %d, want 1", m.Start.ByteOffset) 2857 + } 2858 + if m.End.ByteOffset != 4 { 2859 + t.Fatalf("got match end %d, want 4", m.End.ByteOffset) 2860 + } 2861 + }) 1869 2862 } 1870 2863 1871 2864 func TestSymbolRegexpAll(t *testing.T) { ··· 1888 2881 q := &query.Symbol{ 1889 2882 Expr: &query.Regexp{Regexp: mustParseRE(".*")}, 1890 2883 } 1891 - res := searchForTest(t, b, q) 1892 - if len(res.Files) != len(docs) { 1893 - t.Fatalf("got %v, want %d file", res.Files, len(docs)) 1894 - } 1895 - for i, want := range docs { 1896 - got := res.Files[i].LineMatches[0].LineFragments 1897 - if len(got) != len(want.Symbols) { 1898 - t.Fatalf("got %d symbols, want %d symbols in doc %s", len(got), len(want.Symbols), want.Name) 2884 + t.Run("LineMatches", func(t *testing.T) { 2885 + res := searchForTest(t, b, q) 2886 + if len(res.Files) != len(docs) { 2887 + t.Fatalf("got %v, want %d file", res.Files, len(docs)) 1899 2888 } 2889 + for i, want := range docs { 2890 + got := res.Files[i].LineMatches[0].LineFragments 2891 + if len(got) != len(want.Symbols) { 2892 + t.Fatalf("got %d symbols, want %d symbols in doc %s", len(got), len(want.Symbols), want.Name) 2893 + } 1900 2894 1901 - for j, sec := range want.Symbols { 1902 - if sec.Start != got[j].Offset { 1903 - t.Fatalf("got offset %d, want %d in doc %s", got[j].Offset, sec.Start, want.Name) 2895 + for j, sec := range want.Symbols { 2896 + if sec.Start != got[j].Offset { 2897 + t.Fatalf("got offset %d, want %d in doc %s", got[j].Offset, sec.Start, want.Name) 2898 + } 2899 + } 2900 + } 2901 + }) 2902 + 2903 + t.Run("ChunkMatches", func(t *testing.T) { 2904 + res := searchForTest(t, b, q, chunkOpts) 2905 + if len(res.Files) != len(docs) { 2906 + t.Fatalf("got %v, want %d file", res.Files, len(docs)) 2907 + } 2908 + for i, want := range docs { 2909 + got := res.Files[i].ChunkMatches[0].Ranges 2910 + if len(got) != len(want.Symbols) { 2911 + t.Fatalf("got %d symbols, want %d symbols in doc %s", len(got), len(want.Symbols), want.Name) 2912 + } 2913 + 2914 + for j, sec := range want.Symbols { 2915 + if sec.Start != uint32(got[j].Start.ByteOffset) { 2916 + t.Fatalf("got offset %d, want %d in doc %s", got[j].Start.ByteOffset, sec.Start, want.Name) 2917 + } 1904 2918 } 1905 2919 } 1906 - } 2920 + }) 1907 2921 } 1908 2922 1909 2923 func TestHitIterTerminate(t *testing.T) { ··· 1918 2932 Content: content, 1919 2933 }, 1920 2934 ) 1921 - searchForTest(t, b, &query.Substring{Pattern: "abcdef"}) 2935 + 2936 + t.Run("LineMatches", func(t *testing.T) { 2937 + searchForTest(t, b, &query.Substring{Pattern: "abcdef"}) 2938 + }) 2939 + 2940 + t.Run("ChunkMatches", func(t *testing.T) { 2941 + searchForTest(t, b, &query.Substring{Pattern: "abcdef"}, chunkOpts) 2942 + }) 1922 2943 } 1923 2944 1924 2945 func TestDistanceHitIterBailLast(t *testing.T) { ··· 1929 2950 Content: content, 1930 2951 }, 1931 2952 ) 1932 - res := searchForTest(t, b, &query.Substring{Pattern: "UAST"}) 1933 - if len(res.Files) != 0 { 1934 - t.Fatalf("got %v, want no results", res.Files) 1935 - } 2953 + t.Run("LineMatches", func(t *testing.T) { 2954 + res := searchForTest(t, b, &query.Substring{Pattern: "UAST"}) 2955 + if len(res.Files) != 0 { 2956 + t.Fatalf("got %v, want no results", res.Files) 2957 + } 2958 + }) 2959 + 2960 + t.Run("LineMatches", func(t *testing.T) { 2961 + res := searchForTest(t, b, &query.Substring{Pattern: "UAST"}, chunkOpts) 2962 + if len(res.Files) != 0 { 2963 + t.Fatalf("got %v, want no results", res.Files) 2964 + } 2965 + }) 1936 2966 } 1937 2967 1938 2968 func TestDocumentSectionRuneBoundary(t *testing.T) { ··· 1966 2996 ) 1967 2997 1968 2998 q := &query.Substring{Pattern: content} 1969 - res := searchForTest(t, b, q) 1970 - if len(res.Files) != 1 { 1971 - t.Fatalf("want 1 match, got %v", res.Files) 1972 - } 2999 + 3000 + t.Run("LineMatches", func(t *testing.T) { 3001 + res := searchForTest(t, b, q) 3002 + if len(res.Files) != 1 { 3003 + t.Fatalf("want 1 match, got %v", res.Files) 3004 + } 3005 + 3006 + f := res.Files[0] 3007 + if len(f.LineMatches) != 1 { 3008 + t.Fatalf("want 1 line, got %v", f.LineMatches) 3009 + } 3010 + l := f.LineMatches[0] 3011 + 3012 + if len(l.LineFragments) != 1 { 3013 + t.Fatalf("want 1 line fragment, got %v", l.LineFragments) 3014 + } 3015 + fr := l.LineFragments[0] 3016 + if fr.MatchLength != len(content) { 3017 + t.Fatalf("got MatchLength %d want %d", fr.MatchLength, len(content)) 3018 + } 3019 + }) 1973 3020 1974 - f := res.Files[0] 1975 - if len(f.LineMatches) != 1 { 1976 - t.Fatalf("want 1 line, got %v", f.LineMatches) 1977 - } 1978 - l := f.LineMatches[0] 3021 + t.Run("ChunkMatches", func(t *testing.T) { 3022 + res := searchForTest(t, b, q, chunkOpts) 3023 + if len(res.Files) != 1 { 3024 + t.Fatalf("want 1 match, got %v", res.Files) 3025 + } 1979 3026 1980 - if len(l.LineFragments) != 1 { 1981 - t.Fatalf("want 1 line fragment, got %v", l.LineFragments) 1982 - } 1983 - fr := l.LineFragments[0] 1984 - if fr.MatchLength != len(content) { 1985 - t.Fatalf("got MatchLength %d want %d", fr.MatchLength, len(content)) 1986 - } 3027 + f := res.Files[0] 3028 + if len(f.ChunkMatches) != 1 { 3029 + t.Fatalf("want 1 line, got %v", f.LineMatches) 3030 + } 3031 + cm := f.ChunkMatches[0] 3032 + 3033 + if len(cm.Ranges) != 1 { 3034 + t.Fatalf("want 1 line fragment, got %v", cm.Ranges) 3035 + } 3036 + rr := cm.Ranges[0] 3037 + if matchLen := rr.End.ByteOffset - rr.Start.ByteOffset; int(matchLen) != len(content) { 3038 + t.Fatalf("got MatchLength %d want %d", matchLen, len(content)) 3039 + } 3040 + }) 1987 3041 } 1988 3042 1989 3043 func TestSkipInvalidContent(t *testing.T) { ··· 2004 3058 t.Fatal(err) 2005 3059 } 2006 3060 2007 - q := &query.Substring{Pattern: "abc def"} 2008 - res := searchForTest(t, b, q) 2009 - if len(res.Files) != 0 { 2010 - t.Fatalf("got %v, want no results", res.Files) 2011 - } 3061 + t.Run("LineMatches", func(t *testing.T) { 3062 + q := &query.Substring{Pattern: "abc def"} 3063 + res := searchForTest(t, b, q) 3064 + if len(res.Files) != 0 { 3065 + t.Fatalf("got %v, want no results", res.Files) 3066 + } 3067 + 3068 + q = &query.Substring{Pattern: "NOT-INDEXED"} 3069 + res = searchForTest(t, b, q) 3070 + if len(res.Files) != 1 { 3071 + t.Fatalf("got %v, want 1 result", res.Files) 3072 + } 3073 + }) 2012 3074 2013 - q = &query.Substring{Pattern: "NOT-INDEXED"} 2014 - res = searchForTest(t, b, q) 2015 - if len(res.Files) != 1 { 2016 - t.Fatalf("got %v, want 1 result", res.Files) 2017 - } 3075 + t.Run("ChunkMatches", func(t *testing.T) { 3076 + q := &query.Substring{Pattern: "abc def"} 3077 + res := searchForTest(t, b, q, chunkOpts) 3078 + if len(res.Files) != 0 { 3079 + t.Fatalf("got %v, want no results", res.Files) 3080 + } 3081 + 3082 + q = &query.Substring{Pattern: "NOT-INDEXED"} 3083 + res = searchForTest(t, b, q, chunkOpts) 3084 + if len(res.Files) != 1 { 3085 + t.Fatalf("got %v, want 1 result", res.Files) 3086 + } 3087 + }) 2018 3088 } 2019 3089 } 2020 3090 ··· 2044 3114 Regexp: r, 2045 3115 Content: true, 2046 3116 } 2047 - res := searchForTest(t, b, &q) 2048 - wantRegexpCount := 1 2049 - if gotRegexpCount := res.RegexpsConsidered; gotRegexpCount != wantRegexpCount { 2050 - t.Errorf("got %d, wanted %d", gotRegexpCount, wantRegexpCount) 2051 - } 2052 - if len(res.Files) != 1 || res.Files[0].FileName != "f1" { 2053 - t.Errorf("got %v, want 1 result", res.Files) 2054 - } 3117 + t.Run("LineMatches", func(t *testing.T) { 3118 + res := searchForTest(t, b, &q) 3119 + wantRegexpCount := 1 3120 + if gotRegexpCount := res.RegexpsConsidered; gotRegexpCount != wantRegexpCount { 3121 + t.Errorf("got %d, wanted %d", gotRegexpCount, wantRegexpCount) 3122 + } 3123 + if len(res.Files) != 1 || res.Files[0].FileName != "f1" { 3124 + t.Errorf("got %v, want 1 result", res.Files) 3125 + } 3126 + }) 3127 + 3128 + t.Run("ChunkMatches", func(t *testing.T) { 3129 + res := searchForTest(t, b, &q, chunkOpts) 3130 + wantRegexpCount := 1 3131 + if gotRegexpCount := res.RegexpsConsidered; gotRegexpCount != wantRegexpCount { 3132 + t.Errorf("got %d, wanted %d", gotRegexpCount, wantRegexpCount) 3133 + } 3134 + if len(res.Files) != 1 || res.Files[0].FileName != "f1" { 3135 + t.Errorf("got %v, want 1 result", res.Files) 3136 + } 3137 + }) 2055 3138 } 2056 3139 2057 3140 func TestLineAndFileName(t *testing.T) { ··· 2067 3150 Regexp: r, 2068 3151 FileName: true, 2069 3152 } 2070 - res := searchForTest(t, b, &q) 2071 - wantRegexpCount := 1 2072 - if gotRegexpCount := res.RegexpsConsidered; gotRegexpCount != wantRegexpCount { 2073 - t.Errorf("got %d, wanted %d", gotRegexpCount, wantRegexpCount) 2074 - } 2075 - if len(res.Files) != 1 || res.Files[0].FileName != "apple banana" { 2076 - t.Errorf("got %v, want 1 result", res.Files) 2077 - } 3153 + t.Run("LineMatches", func(t *testing.T) { 3154 + res := searchForTest(t, b, &q) 3155 + wantRegexpCount := 1 3156 + if gotRegexpCount := res.RegexpsConsidered; gotRegexpCount != wantRegexpCount { 3157 + t.Errorf("got %d, wanted %d", gotRegexpCount, wantRegexpCount) 3158 + } 3159 + if len(res.Files) != 1 || res.Files[0].FileName != "apple banana" { 3160 + t.Errorf("got %v, want 1 result", res.Files) 3161 + } 3162 + }) 3163 + 3164 + t.Run("ChunkMatches", func(t *testing.T) { 3165 + res := searchForTest(t, b, &q, chunkOpts) 3166 + wantRegexpCount := 1 3167 + if gotRegexpCount := res.RegexpsConsidered; gotRegexpCount != wantRegexpCount { 3168 + t.Errorf("got %d, wanted %d", gotRegexpCount, wantRegexpCount) 3169 + } 3170 + if len(res.Files) != 1 || res.Files[0].FileName != "apple banana" { 3171 + t.Errorf("got %v, want 1 result", res.Files) 3172 + } 3173 + }) 2078 3174 } 2079 3175 2080 3176 func TestMultiLineRegex(t *testing.T) { ··· 2089 3185 q := query.Regexp{ 2090 3186 Regexp: r, 2091 3187 } 2092 - res := searchForTest(t, b, &q) 2093 - wantRegexpCount := 2 2094 - if gotRegexpCount := res.RegexpsConsidered; gotRegexpCount != wantRegexpCount { 2095 - t.Errorf("got %d, wanted %d", gotRegexpCount, wantRegexpCount) 2096 - } 2097 - if len(res.Files) != 1 || res.Files[0].FileName != "f1" { 2098 - t.Errorf("got %v, want 1 result", res.Files) 2099 - } 3188 + t.Run("LineMatches", func(t *testing.T) { 3189 + res := searchForTest(t, b, &q) 3190 + wantRegexpCount := 2 3191 + if gotRegexpCount := res.RegexpsConsidered; gotRegexpCount != wantRegexpCount { 3192 + t.Errorf("got %d, wanted %d", gotRegexpCount, wantRegexpCount) 3193 + } 3194 + if len(res.Files) != 1 || res.Files[0].FileName != "f1" { 3195 + t.Errorf("got %v, want 1 result", res.Files) 3196 + } 3197 + if l := len(res.Files[0].LineMatches); l != 2 { 3198 + t.Errorf("got %v, want 2 line matches", l) 3199 + } 3200 + }) 3201 + 3202 + t.Run("ChunkMatches", func(t *testing.T) { 3203 + res := searchForTest(t, b, &q, chunkOpts) 3204 + wantRegexpCount := 2 3205 + if gotRegexpCount := res.RegexpsConsidered; gotRegexpCount != wantRegexpCount { 3206 + t.Errorf("got %d, wanted %d", gotRegexpCount, wantRegexpCount) 3207 + } 3208 + if len(res.Files) != 1 || res.Files[0].FileName != "f1" { 3209 + t.Errorf("got %v, want 1 result", res.Files) 3210 + } 3211 + if l := len(res.Files[0].ChunkMatches); l != 1 { 3212 + t.Errorf("got %v, want 1 chunk matches", l) 3213 + } 3214 + if l := len(res.Files[0].ChunkMatches[0].Ranges); l != 1 { 3215 + t.Errorf("got %v, want 1 chunk ranges", l) 3216 + } 3217 + }) 2100 3218 } 2101 3219 2102 3220 func TestSearchTypeFileName(t *testing.T) { ··· 2108 3226 // -----------------------------------012345678901234567890-123456 2109 3227 ) 2110 3228 2111 - wantSingleMatch := func(res *SearchResult, want string) { 2112 - t.Helper() 2113 - fmatches := res.Files 2114 - if len(fmatches) != 1 { 2115 - t.Errorf("got %v, want 1 matches", len(fmatches)) 2116 - return 3229 + t.Run("LineMatches", func(t *testing.T) { 3230 + wantSingleMatch := func(res *SearchResult, want string) { 3231 + t.Helper() 3232 + fmatches := res.Files 3233 + if len(fmatches) != 1 { 3234 + t.Errorf("got %v, want 1 matches", len(fmatches)) 3235 + return 3236 + } 3237 + if len(fmatches[0].LineMatches) != 1 { 3238 + t.Errorf("got %d line matches", len(fmatches[0].LineMatches)) 3239 + return 3240 + } 3241 + var got string 3242 + if fmatches[0].LineMatches[0].FileName { 3243 + got = fmatches[0].FileName 3244 + } else { 3245 + got = fmt.Sprintf("%s:%d", fmatches[0].FileName, fmatches[0].LineMatches[0].LineFragments[0].Offset) 3246 + } 3247 + 3248 + if got != want { 3249 + t.Errorf("got %s, want %s", got, want) 3250 + } 2117 3251 } 2118 - if len(fmatches[0].LineMatches) != 1 { 2119 - t.Errorf("got %d line matches", len(fmatches[0].LineMatches)) 2120 - return 2121 - } 2122 - var got string 2123 - if fmatches[0].LineMatches[0].FileName { 2124 - got = fmatches[0].FileName 2125 - } else { 2126 - got = fmt.Sprintf("%s:%d", fmatches[0].FileName, fmatches[0].LineMatches[0].LineFragments[0].Offset) 2127 - } 3252 + 3253 + // Only return the later match in the second file 3254 + res := searchForTest(t, b, query.NewAnd( 3255 + &query.Type{ 3256 + Type: query.TypeFileName, 3257 + Child: &query.Substring{Pattern: "needle"}, 3258 + }, 3259 + &query.Substring{Pattern: "file"})) 3260 + wantSingleMatch(res, "f2:8") 3261 + 3262 + // Only return a filename result 3263 + res = searchForTest(t, b, 3264 + &query.Type{ 3265 + Type: query.TypeFileName, 3266 + Child: &query.Substring{Pattern: "file"}, 3267 + }) 3268 + wantSingleMatch(res, "f2") 3269 + }) 3270 + 3271 + t.Run("ChunkMatches", func(t *testing.T) { 3272 + wantSingleMatch := func(res *SearchResult, want string) { 3273 + t.Helper() 3274 + fmatches := res.Files 3275 + if len(fmatches) != 1 { 3276 + t.Errorf("got %v, want 1 matches", len(fmatches)) 3277 + return 3278 + } 3279 + if len(fmatches[0].ChunkMatches) != 1 { 3280 + t.Errorf("got %d line matches", len(fmatches[0].ChunkMatches)) 3281 + return 3282 + } 3283 + var got string 3284 + if fmatches[0].ChunkMatches[0].FileName { 3285 + got = fmatches[0].FileName 3286 + } else { 3287 + got = fmt.Sprintf("%s:%d", fmatches[0].FileName, fmatches[0].ChunkMatches[0].Ranges[0].Start.ByteOffset) 3288 + } 2128 3289 2129 - if got != want { 2130 - t.Errorf("got %s, want %s", got, want) 3290 + if got != want { 3291 + t.Errorf("got %s, want %s", got, want) 3292 + } 2131 3293 } 2132 - } 2133 3294 2134 - // Only return the later match in the second file 2135 - res := searchForTest(t, b, query.NewAnd( 2136 - &query.Type{ 2137 - Type: query.TypeFileName, 2138 - Child: &query.Substring{Pattern: "needle"}, 2139 - }, 2140 - &query.Substring{Pattern: "file"})) 2141 - wantSingleMatch(res, "f2:8") 3295 + // Only return the later match in the second file 3296 + res := searchForTest(t, b, query.NewAnd( 3297 + &query.Type{ 3298 + Type: query.TypeFileName, 3299 + Child: &query.Substring{Pattern: "needle"}, 3300 + }, 3301 + &query.Substring{Pattern: "file"}), 3302 + chunkOpts, 3303 + ) 3304 + wantSingleMatch(res, "f2:8") 2142 3305 2143 - // Only return a filename result 2144 - res = searchForTest(t, b, 2145 - &query.Type{ 2146 - Type: query.TypeFileName, 2147 - Child: &query.Substring{Pattern: "file"}, 2148 - }) 2149 - wantSingleMatch(res, "f2") 3306 + // Only return a filename result 3307 + res = searchForTest(t, b, 3308 + &query.Type{ 3309 + Type: query.TypeFileName, 3310 + Child: &query.Substring{Pattern: "file"}, 3311 + }, 3312 + chunkOpts, 3313 + ) 3314 + wantSingleMatch(res, "f2") 3315 + }) 2150 3316 } 2151 3317 2152 3318 func TestSearchTypeLanguage(t *testing.T) { ··· 2160 3326 2161 3327 t.Log(b.languageMap) 2162 3328 2163 - wantSingleMatch := func(res *SearchResult, want string) { 2164 - t.Helper() 2165 - fmatches := res.Files 2166 - if len(fmatches) != 1 { 2167 - t.Errorf("got %v, want 1 matches", len(fmatches)) 2168 - return 2169 - } 2170 - if len(fmatches[0].LineMatches) != 1 { 2171 - t.Errorf("got %d line matches", len(fmatches[0].LineMatches)) 2172 - return 3329 + t.Run("LineMatches", func(t *testing.T) { 3330 + wantSingleMatch := func(res *SearchResult, want string) { 3331 + t.Helper() 3332 + fmatches := res.Files 3333 + if len(fmatches) != 1 { 3334 + t.Errorf("got %v, want 1 matches", len(fmatches)) 3335 + return 3336 + } 3337 + if len(fmatches[0].LineMatches) != 1 { 3338 + t.Errorf("got %d line matches", len(fmatches[0].LineMatches)) 3339 + return 3340 + } 3341 + var got string 3342 + if fmatches[0].LineMatches[0].FileName { 3343 + got = fmatches[0].FileName 3344 + } else { 3345 + got = fmt.Sprintf("%s:%d", fmatches[0].FileName, fmatches[0].LineMatches[0].LineFragments[0].Offset) 3346 + } 3347 + 3348 + if got != want { 3349 + t.Errorf("got %s, want %s", got, want) 3350 + } 2173 3351 } 2174 - var got string 2175 - if fmatches[0].LineMatches[0].FileName { 2176 - got = fmatches[0].FileName 2177 - } else { 2178 - got = fmt.Sprintf("%s:%d", fmatches[0].FileName, fmatches[0].LineMatches[0].LineFragments[0].Offset) 3352 + 3353 + res := searchForTest(t, b, &query.Language{Language: "Apex"}) 3354 + wantSingleMatch(res, "apex.cls") 3355 + 3356 + res = searchForTest(t, b, &query.Language{Language: "TeX"}) 3357 + wantSingleMatch(res, "tex.cls") 3358 + 3359 + res = searchForTest(t, b, &query.Language{Language: "C"}) 3360 + wantSingleMatch(res, "hello.h") 3361 + 3362 + // test fallback language search by pretending it's an older index version 3363 + res = searchForTest(t, b, &query.Language{Language: "C++"}) 3364 + if len(res.Files) != 0 { 3365 + t.Errorf("got %d results for C++, want 0", len(res.Files)) 2179 3366 } 2180 3367 2181 - if got != want { 2182 - t.Errorf("got %s, want %s", got, want) 3368 + b.featureVersion = 11 // force fallback 3369 + res = searchForTest(t, b, &query.Language{Language: "C++"}) 3370 + wantSingleMatch(res, "hello.h") 3371 + }) 3372 + 3373 + t.Run("ChunkMatches", func(t *testing.T) { 3374 + wantSingleMatch := func(res *SearchResult, want string) { 3375 + t.Helper() 3376 + fmatches := res.Files 3377 + if len(fmatches) != 1 { 3378 + t.Errorf("got %v, want 1 matches", len(fmatches)) 3379 + return 3380 + } 3381 + if len(fmatches[0].ChunkMatches) != 1 { 3382 + t.Errorf("got %d line matches", len(fmatches[0].ChunkMatches)) 3383 + return 3384 + } 3385 + var got string 3386 + if fmatches[0].ChunkMatches[0].FileName { 3387 + got = fmatches[0].FileName 3388 + } else { 3389 + got = fmt.Sprintf("%s:%d", fmatches[0].FileName, fmatches[0].ChunkMatches[0].Ranges[0].Start.ByteOffset) 3390 + } 3391 + 3392 + if got != want { 3393 + t.Errorf("got %s, want %s", got, want) 3394 + } 2183 3395 } 2184 - } 2185 3396 2186 - res := searchForTest(t, b, &query.Language{Language: "Apex"}) 2187 - wantSingleMatch(res, "apex.cls") 3397 + b.featureVersion = FeatureVersion // reset feature version 3398 + res := searchForTest(t, b, &query.Language{Language: "Apex"}, chunkOpts) 3399 + wantSingleMatch(res, "apex.cls") 2188 3400 2189 - res = searchForTest(t, b, &query.Language{Language: "TeX"}) 2190 - wantSingleMatch(res, "tex.cls") 3401 + res = searchForTest(t, b, &query.Language{Language: "TeX"}, chunkOpts) 3402 + wantSingleMatch(res, "tex.cls") 2191 3403 2192 - res = searchForTest(t, b, &query.Language{Language: "C"}) 2193 - wantSingleMatch(res, "hello.h") 3404 + res = searchForTest(t, b, &query.Language{Language: "C"}, chunkOpts) 3405 + wantSingleMatch(res, "hello.h") 2194 3406 2195 - // test fallback language search by pretending it's an older index version 2196 - res = searchForTest(t, b, &query.Language{Language: "C++"}) 2197 - if len(res.Files) != 0 { 2198 - t.Errorf("got %d results for C++, want 0", len(res.Files)) 2199 - } 3407 + // test fallback language search by pretending it's an older index version 3408 + res = searchForTest(t, b, &query.Language{Language: "C++"}, chunkOpts) 3409 + if len(res.Files) != 0 { 3410 + t.Errorf("got %d results for C++, want 0", len(res.Files)) 3411 + } 2200 3412 2201 - b.featureVersion = 11 // force fallback 2202 - res = searchForTest(t, b, &query.Language{Language: "C++"}) 2203 - wantSingleMatch(res, "hello.h") 3413 + b.featureVersion = 11 // force fallback 3414 + res = searchForTest(t, b, &query.Language{Language: "C++"}, chunkOpts) 3415 + wantSingleMatch(res, "hello.h") 3416 + }) 2204 3417 } 2205 3418 2206 3419 func TestStats(t *testing.T) {
+3
shards/shards.go
··· 734 734 copySlice(&sr.Files[i].LineMatches[l].Before) 735 735 copySlice(&sr.Files[i].LineMatches[l].After) 736 736 } 737 + for c := range sr.Files[i].ChunkMatches { 738 + copySlice(&sr.Files[i].ChunkMatches[c].Content) 739 + } 737 740 } 738 741 } 739 742