Adds a test for search-based usages (#63610)

Closes
https://linear.app/sourcegraph/issue/GRAPH-726/test-syntactic-and-search-based-usages

Testing just the search-based usages just requires mocking the
SearchClient, which works out nicely.

## Test plan

The whole PR is just a test
This commit is contained in:
Christoph Hegemann 2024-07-10 15:22:53 +02:00 committed by GitHub
parent 2645a9b04d
commit d3df71ef98
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 223 additions and 6 deletions

View File

@ -70,7 +70,9 @@ go_test(
"service_references_test.go",
"service_snapshot_test.go",
"service_stencil_test.go",
"service_syntactic_usages_test.go",
"service_test.go",
"utils_test.go",
],
embed = [":codenav"],
tags = [TAG_PLATFORM_GRAPH],
@ -87,10 +89,14 @@ go_test(
"//internal/gitserver",
"//internal/gitserver/gitdomain",
"//internal/observation",
"//internal/search",
"//internal/search/client",
"//internal/search/result",
"//internal/search/streaming",
"//internal/types",
"//lib/codeintel/precise",
"@com_github_google_go_cmp//cmp",
"@com_github_life4_genesis//slices",
"@com_github_sourcegraph_go_diff//diff",
"@com_github_sourcegraph_log//:log",
"@com_github_sourcegraph_scip//bindings/go/scip",

View File

@ -1341,6 +1341,19 @@ func (s *Service) SearchBasedUsages(
}
}
return s.searchBasedUsagesInner(ctx, trace, args, symbolName, language, syntacticUploadID)
}
// searchBasedUsagesInner is extracted from SearchBasedUsages to allow
// testing of the core logic, by only mocking the search client.
func (s *Service) searchBasedUsagesInner(
ctx context.Context,
trace observation.TraceLogger,
args UsagesForSymbolArgs,
symbolName string,
language string,
syntacticUploadID core.Option[int],
) (matches []SearchBasedMatch, err error) {
searchCoords := searchArgs{
repo: args.Repo.Name,
commit: args.Commit,

View File

@ -0,0 +1,99 @@
package codenav
import (
"context"
"testing"
genslices "github.com/life4/genesis/slices"
"github.com/sourcegraph/log"
"github.com/sourcegraph/scip/bindings/go/scip"
"github.com/stretchr/testify/require"
"github.com/sourcegraph/sourcegraph/internal/codeintel/core"
"github.com/sourcegraph/sourcegraph/internal/gitserver"
"github.com/sourcegraph/sourcegraph/internal/observation"
)
func expectSearchRanges(t *testing.T, matches []SearchBasedMatch, ranges ...scip.Range) {
t.Helper()
for _, match := range matches {
_, err := genslices.Find(ranges, func(r scip.Range) bool {
return match.Range.CompareStrict(r) == 0
})
if err != nil {
t.Errorf("Did not expect match at %q", match.Range.String())
}
}
for _, r := range ranges {
_, err := genslices.Find(matches, func(match SearchBasedMatch) bool {
return match.Range.CompareStrict(r) == 0
})
if err != nil {
t.Errorf("Expected match at %q", r.String())
}
}
}
func expectDefinitionRanges(t *testing.T, matches []SearchBasedMatch, ranges ...scip.Range) {
t.Helper()
for _, match := range matches {
_, err := genslices.Find(ranges, func(r scip.Range) bool {
return match.Range.CompareStrict(r) == 0
})
if match.IsDefinition && err != nil {
t.Errorf("Did not expect match at %q to be a definition", match.Range.String())
return
} else if !match.IsDefinition && err == nil {
t.Errorf("Expected match at %q to be a definition", match.Range.String())
return
}
}
}
func TestSearchBasedUsages_ResultWithoutSymbols(t *testing.T) {
refRange := scipRange(1)
refRange2 := scipRange(2)
mockSearchClient := FakeSearchClient().
WithFile("path.java", refRange, refRange2).
Build()
svc := newService(
observation.TestContextTB(t), defaultMockRepoStore(),
NewMockLsifStore(), NewMockUploadService(), gitserver.NewMockClient(),
mockSearchClient, log.NoOp(),
)
usages, searchErr := svc.searchBasedUsagesInner(
context.Background(), observation.TestTraceLogger(log.NoOp()), UsagesForSymbolArgs{},
"symbol", "Java", core.None[int](),
)
require.NoError(t, searchErr)
expectSearchRanges(t, usages, refRange, refRange2)
}
func TestSearchBasedUsages_ResultWithSymbol(t *testing.T) {
refRange := scipRange(1)
defRange := scipRange(2)
refRange2 := scipRange(3)
mockSearchClient := FakeSearchClient().
WithFile("path.java", refRange, refRange2, defRange).
WithSymbols("path.java", defRange).
Build()
svc := newService(
observation.TestContextTB(t), defaultMockRepoStore(),
NewMockLsifStore(), NewMockUploadService(), gitserver.NewMockClient(),
mockSearchClient, log.NoOp(),
)
usages, searchErr := svc.searchBasedUsagesInner(
context.Background(), observation.TestTraceLogger(log.NoOp()), UsagesForSymbolArgs{},
"symbol", "Java", core.None[int](),
)
require.NoError(t, searchErr)
expectSearchRanges(t, usages, refRange, refRange2, defRange)
expectDefinitionRanges(t, usages, defRange)
}

View File

@ -73,12 +73,7 @@ func findCandidateOccurrencesViaSearch(
inconsistentFilepaths = 1
continue
}
scipRange, err := scip.NewRange([]int32{
int32(matchRange.Start.Line),
int32(matchRange.Start.Column),
int32(matchRange.End.Line),
int32(matchRange.End.Column),
})
scipRange, err := scipFromResultRange(matchRange)
if err != nil {
trace.Warn("Failed to create scip range from match range",
log.String("error", err.Error()),
@ -246,3 +241,12 @@ func sliceRangeFromReader(reader io.Reader, range_ scip.Range) (substr string, e
}
return "", errors.New("symbol range is out-of-bounds")
}
func scipFromResultRange(resultRange result.Range) (scip.Range, error) {
return scip.NewRange([]int32{
int32(resultRange.Start.Line),
int32(resultRange.Start.Column),
int32(resultRange.End.Line),
int32(resultRange.End.Column),
})
}

View File

@ -0,0 +1,95 @@
// This file does not contain tests for utils.go, instead it contains utils
// for setting up the test environment for our codenav tests.
package codenav
import (
"context"
"strings"
genslices "github.com/life4/genesis/slices"
"github.com/sourcegraph/scip/bindings/go/scip"
"github.com/sourcegraph/sourcegraph/internal/search"
searchClient "github.com/sourcegraph/sourcegraph/internal/search/client"
"github.com/sourcegraph/sourcegraph/internal/search/result"
"github.com/sourcegraph/sourcegraph/internal/search/streaming"
)
// Generates a fake scip.Range that is easy to tell from other ranges
func scipRange(x int32) scip.Range {
return scip.NewRangeUnchecked([]int32{x, x, x})
}
func scipToResultPosition(p scip.Position) result.Location {
return result.Location{
Line: int(p.Line),
Column: int(p.Character),
}
}
func scipToResultRange(r scip.Range) result.Range {
return result.Range{
Start: scipToResultPosition(r.Start),
End: scipToResultPosition(r.End),
}
}
// scipToSymbolMatch "reverse engineers" the lsp.Range function on result.Symbol
func scipToSymbolMatch(r scip.Range) *result.SymbolMatch {
return &result.SymbolMatch{
Symbol: result.Symbol{
Line: int(r.Start.Line + 1),
Character: int(r.Start.Character),
Name: strings.Repeat("a", int(r.End.Character-r.Start.Character)),
}}
}
type FakeSearchBuilder struct {
fileMatches []result.Match
symbolMatches []result.Match
}
func FakeSearchClient() FakeSearchBuilder {
return FakeSearchBuilder{
fileMatches: []result.Match{},
symbolMatches: make([]result.Match, 0),
}
}
func (b FakeSearchBuilder) WithFile(file string, ranges ...scip.Range) FakeSearchBuilder {
b.fileMatches = append(b.fileMatches, &result.FileMatch{
File: result.File{Path: file},
ChunkMatches: result.ChunkMatches{{
Ranges: genslices.Map(ranges, scipToResultRange),
}},
})
return b
}
func (b FakeSearchBuilder) WithSymbols(file string, ranges ...scip.Range) FakeSearchBuilder {
b.symbolMatches = append(b.symbolMatches, &result.FileMatch{
File: result.File{Path: file},
Symbols: genslices.Map(ranges, scipToSymbolMatch),
})
return b
}
func (b FakeSearchBuilder) Build() searchClient.SearchClient {
mockSearchClient := searchClient.NewMockSearchClient()
mockSearchClient.PlanFunc.SetDefaultHook(func(_ context.Context, _ string, _ *string, query string, _ search.Mode, _ search.Protocol, _ *int32) (*search.Inputs, error) {
return &search.Inputs{OriginalQuery: query}, nil
})
mockSearchClient.ExecuteFunc.SetDefaultHook(func(_ context.Context, s streaming.Sender, i *search.Inputs) (*search.Alert, error) {
if strings.Contains(i.OriginalQuery, "type:file") {
s.Send(streaming.SearchEvent{
Results: b.fileMatches,
})
} else if strings.Contains(i.OriginalQuery, "type:symbol") {
s.Send(streaming.SearchEvent{
Results: b.symbolMatches,
})
}
return nil, nil
})
return mockSearchClient
}