internal/lsp: reorganize completion tests

Our completion tests check for a lot of different behaviors. It may be
easier to develop if we have separate tests for things like deep
completion and completion snippets.

Change-Id: I7f4b0c0e52670f2a6c00247199933fd1ffa0096f
Reviewed-on: https://go-review.googlesource.com/c/tools/+/196021
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Ian Cottrell <iancottrell@google.com>
This commit is contained in:
Rebecca Stambler 2019-09-17 11:10:48 -04:00
parent 3ac2a5bbd9
commit c006dc79eb
13 changed files with 820 additions and 609 deletions

View File

@ -33,7 +33,27 @@ func NewRunner(exporter packagestest.Exporter, data *tests.Data, ctx context.Con
} }
} }
func (r *runner) Completion(t *testing.T, data tests.Completions, snippets tests.CompletionSnippets, items tests.CompletionItems) { func (r *runner) Completion(t *testing.T, data tests.Completions, items tests.CompletionItems) {
//TODO: add command line completions tests when it works
}
func (r *runner) CompletionSnippets(t *testing.T, data tests.CompletionSnippets, items tests.CompletionItems) {
//TODO: add command line completions tests when it works
}
func (r *runner) UnimportedCompletions(t *testing.T, data tests.UnimportedCompletions, items tests.CompletionItems) {
//TODO: add command line completions tests when it works
}
func (r *runner) DeepCompletions(t *testing.T, data tests.DeepCompletions, items tests.CompletionItems) {
//TODO: add command line completions tests when it works
}
func (r *runner) FuzzyCompletions(t *testing.T, data tests.FuzzyCompletions, items tests.CompletionItems) {
//TODO: add command line completions tests when it works
}
func (r *runner) RankCompletions(t *testing.T, data tests.RankCompletions, items tests.CompletionItems) {
//TODO: add command line completions tests when it works //TODO: add command line completions tests when it works
} }

View File

@ -47,11 +47,11 @@ func (s *Server) completion(ctx context.Context, params *protocol.CompletionPara
// When using deep completions/fuzzy matching, report results as incomplete so // When using deep completions/fuzzy matching, report results as incomplete so
// client fetches updated completions after every key stroke. // client fetches updated completions after every key stroke.
IsIncomplete: options.Completion.Deep, IsIncomplete: options.Completion.Deep,
Items: s.toProtocolCompletionItems(candidates, rng, options), Items: toProtocolCompletionItems(candidates, rng, options),
}, nil }, nil
} }
func (s *Server) toProtocolCompletionItems(candidates []source.CompletionItem, rng protocol.Range, options source.Options) []protocol.CompletionItem { func toProtocolCompletionItems(candidates []source.CompletionItem, rng protocol.Range, options source.Options) []protocol.CompletionItem {
var ( var (
items = make([]protocol.CompletionItem, 0, len(candidates)) items = make([]protocol.CompletionItem, 0, len(candidates))
numDeepCompletionsSeen int numDeepCompletionsSeen int

View File

@ -0,0 +1,151 @@
package lsp
import (
"strings"
"testing"
"time"
"golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/lsp/source"
"golang.org/x/tools/internal/lsp/tests"
"golang.org/x/tools/internal/span"
)
func (r *runner) Completion(t *testing.T, data tests.Completions, items tests.CompletionItems) {
for src, test := range data {
got := r.callCompletion(t, src, source.CompletionOptions{
Deep: false,
FuzzyMatching: false,
Documentation: true,
})
if !strings.Contains(string(src.URI()), "builtins") {
got = tests.FilterBuiltins(got)
}
want := expected(t, test, items)
if diff := tests.DiffCompletionItems(want, got); diff != "" {
t.Errorf("%s: %s", src, diff)
}
}
}
func (r *runner) CompletionSnippets(t *testing.T, data tests.CompletionSnippets, items tests.CompletionItems) {
for _, placeholders := range []bool{true, false} {
for src, expected := range data {
list := r.callCompletion(t, src, source.CompletionOptions{
Placeholders: placeholders,
Deep: true,
Budget: 5 * time.Second,
FuzzyMatching: true,
})
got := tests.FindItem(list, *items[expected.CompletionItem])
want := expected.PlainSnippet
if placeholders {
want = expected.PlaceholderSnippet
}
if diff := tests.DiffSnippets(want, got); diff != "" {
t.Errorf("%s: %v", src, diff)
}
}
}
}
func (r *runner) UnimportedCompletions(t *testing.T, data tests.UnimportedCompletions, items tests.CompletionItems) {
for src, test := range data {
got := r.callCompletion(t, src, source.CompletionOptions{
Unimported: true,
})
if !strings.Contains(string(src.URI()), "builtins") {
got = tests.FilterBuiltins(got)
}
want := expected(t, test, items)
if diff := tests.DiffCompletionItems(want, got); diff != "" {
t.Errorf("%s: %s", src, diff)
}
}
}
func (r *runner) DeepCompletions(t *testing.T, data tests.DeepCompletions, items tests.CompletionItems) {
for src, test := range data {
got := r.callCompletion(t, src, source.CompletionOptions{
Deep: true,
Budget: 5 * time.Second,
Documentation: true,
})
if !strings.Contains(string(src.URI()), "builtins") {
got = tests.FilterBuiltins(got)
}
want := expected(t, test, items)
if msg := tests.DiffCompletionItems(want, got); msg != "" {
t.Errorf("%s: %s", src, msg)
}
}
}
func (r *runner) FuzzyCompletions(t *testing.T, data tests.FuzzyCompletions, items tests.CompletionItems) {
for src, test := range data {
got := r.callCompletion(t, src, source.CompletionOptions{
FuzzyMatching: true,
Deep: true,
Budget: 5 * time.Second,
})
if !strings.Contains(string(src.URI()), "builtins") {
got = tests.FilterBuiltins(got)
}
want := expected(t, test, items)
if msg := tests.DiffCompletionItems(want, got); msg != "" {
t.Errorf("%s: %s", src, msg)
}
}
}
func (r *runner) RankCompletions(t *testing.T, data tests.RankCompletions, items tests.CompletionItems) {
for src, test := range data {
got := r.callCompletion(t, src, source.CompletionOptions{
FuzzyMatching: true,
Deep: true,
Budget: 5 * time.Second,
})
want := expected(t, test, items)
if msg := tests.CheckCompletionOrder(want, got); msg != "" {
t.Errorf("%s: %s", src, msg)
}
}
}
func expected(t *testing.T, test tests.Completion, items tests.CompletionItems) []protocol.CompletionItem {
t.Helper()
var want []protocol.CompletionItem
for _, pos := range test.CompletionItems {
item := items[pos]
want = append(want, tests.ToProtocolCompletionItem(*item))
}
return want
}
func (r *runner) callCompletion(t *testing.T, src span.Span, options source.CompletionOptions) []protocol.CompletionItem {
t.Helper()
view := r.server.session.ViewOf(src.URI())
original := view.Options()
modified := original
modified.InsertTextFormat = protocol.SnippetTextFormat
modified.Completion = options
view.SetOptions(modified)
defer view.SetOptions(original)
list, err := r.server.Completion(r.ctx, &protocol.CompletionParams{
TextDocumentPositionParams: protocol.TextDocumentPositionParams{
TextDocument: protocol.TextDocumentIdentifier{
URI: protocol.NewURI(src.URI()),
},
Position: protocol.Position{
Line: float64(src.Start().Line() - 1),
Character: float64(src.Start().Column() - 1),
},
},
})
if err != nil {
t.Fatal(err)
}
return list.Items
}

View File

@ -15,7 +15,6 @@ import (
"sort" "sort"
"strings" "strings"
"testing" "testing"
"time"
"golang.org/x/tools/go/packages/packagestest" "golang.org/x/tools/go/packages/packagestest"
"golang.org/x/tools/internal/lsp/cache" "golang.org/x/tools/internal/lsp/cache"
@ -51,18 +50,7 @@ func testLSP(t *testing.T, exporter packagestest.Exporter) {
cache := cache.New() cache := cache.New()
session := cache.NewSession(ctx) session := cache.NewSession(ctx)
options := session.Options() options := tests.DefaultOptions()
options.SupportedCodeActions = map[source.FileKind]map[protocol.CodeActionKind]bool{
source.Go: {
protocol.SourceOrganizeImports: true,
protocol.QuickFix: true,
},
source.Mod: {},
source.Sum: {},
}
options.HoverKind = source.SynopsisDocumentation
// Crank this up so tests don't flake.
options.Completion.Budget = 5 * time.Second
session.SetOptions(options) session.SetOptions(options)
options.Env = data.Config.Env options.Env = data.Config.Env
session.NewView(ctx, viewName, span.FileURI(data.Config.Dir), options) session.NewView(ctx, viewName, span.FileURI(data.Config.Dir), options)
@ -111,218 +99,6 @@ func (r *runner) Diagnostics(t *testing.T, data tests.Diagnostics) {
} }
} }
func (r *runner) Completion(t *testing.T, data tests.Completions, snippets tests.CompletionSnippets, items tests.CompletionItems) {
for src, test := range data {
view := r.server.session.ViewOf(src.URI())
original := view.Options()
modified := original
// Set this as a default.
modified.Completion.Documentation = true
var want []source.CompletionItem
for _, pos := range test.CompletionItems {
want = append(want, *items[pos])
}
modified.Completion.Deep = strings.Contains(string(src.URI()), "deepcomplete")
modified.Completion.FuzzyMatching = strings.Contains(string(src.URI()), "fuzzymatch")
modified.Completion.Unimported = strings.Contains(string(src.URI()), "unimported")
view.SetOptions(modified)
list := r.runCompletion(t, src)
wantBuiltins := strings.Contains(string(src.URI()), "builtins")
var got []protocol.CompletionItem
for _, item := range list.Items {
if !wantBuiltins && isBuiltin(item) {
continue
}
got = append(got, item)
}
switch test.Type {
case tests.CompletionFull:
if diff := diffCompletionItems(want, got); diff != "" {
t.Errorf("%s: %s", src, diff)
}
case tests.CompletionPartial:
if msg := checkCompletionOrder(want, got); msg != "" {
t.Errorf("%s: %s", src, msg)
}
}
view.SetOptions(original)
}
for _, usePlaceholders := range []bool{true, false} {
for src, want := range snippets {
view := r.server.session.ViewOf(src.URI())
original := view.Options()
modified := original
modified.InsertTextFormat = protocol.SnippetTextFormat
modified.Completion.Deep = strings.Contains(string(src.URI()), "deepcomplete")
modified.Completion.FuzzyMatching = strings.Contains(string(src.URI()), "fuzzymatch")
modified.Completion.Unimported = strings.Contains(string(src.URI()), "unimported")
modified.Completion.Placeholders = usePlaceholders
view.SetOptions(modified)
list := r.runCompletion(t, src)
wantItem := items[want.CompletionItem]
var got *protocol.CompletionItem
for _, item := range list.Items {
if item.Label == wantItem.Label {
got = &item
break
}
}
var expected string
if usePlaceholders {
expected = want.PlaceholderSnippet
} else {
expected = want.PlainSnippet
}
if expected == "" {
if got != nil {
t.Fatalf("%s:%d: expected no snippet but got %q", src.URI(), src.Start().Line(), got.TextEdit.NewText)
}
} else {
if got == nil {
t.Fatalf("%s:%d: couldn't find completion matching %q", src.URI(), src.Start().Line(), wantItem.Label)
}
if expected != got.TextEdit.NewText {
t.Errorf("%s: expected snippet %q, got %q", src, expected, got.TextEdit.NewText)
}
}
view.SetOptions(original)
}
}
}
func (r *runner) runCompletion(t *testing.T, src span.Span) *protocol.CompletionList {
t.Helper()
list, err := r.server.Completion(r.ctx, &protocol.CompletionParams{
TextDocumentPositionParams: protocol.TextDocumentPositionParams{
TextDocument: protocol.TextDocumentIdentifier{
URI: protocol.NewURI(src.URI()),
},
Position: protocol.Position{
Line: float64(src.Start().Line() - 1),
Character: float64(src.Start().Column() - 1),
},
},
})
if err != nil {
t.Fatal(err)
}
return list
}
func isBuiltin(item protocol.CompletionItem) bool {
// If a type has no detail, it is a builtin type.
if item.Detail == "" && item.Kind == protocol.TypeParameterCompletion {
return true
}
// Remaining builtin constants, variables, interfaces, and functions.
trimmed := item.Label
if i := strings.Index(trimmed, "("); i >= 0 {
trimmed = trimmed[:i]
}
switch trimmed {
case "append", "cap", "close", "complex", "copy", "delete",
"error", "false", "imag", "iota", "len", "make", "new",
"nil", "panic", "print", "println", "real", "recover", "true":
return true
}
return false
}
// diffCompletionItems prints the diff between expected and actual completion
// test results.
func diffCompletionItems(want []source.CompletionItem, got []protocol.CompletionItem) string {
if len(got) != len(want) {
return summarizeCompletionItems(-1, want, got, "different lengths got %v want %v", len(got), len(want))
}
for i, w := range want {
g := got[i]
if w.Label != g.Label {
return summarizeCompletionItems(i, want, got, "incorrect Label got %v want %v", g.Label, w.Label)
}
if w.Detail != g.Detail {
return summarizeCompletionItems(i, want, got, "incorrect Detail got %v want %v", g.Detail, w.Detail)
}
if w.Documentation != "" && !strings.HasPrefix(w.Documentation, "@") {
if w.Documentation != g.Documentation {
return summarizeCompletionItems(i, want, got, "incorrect Documentation got %v want %v", g.Documentation, w.Documentation)
}
}
if wkind := toProtocolCompletionItemKind(w.Kind); wkind != g.Kind {
return summarizeCompletionItems(i, want, got, "incorrect Kind got %v want %v", g.Kind, wkind)
}
}
return ""
}
func checkCompletionOrder(want []source.CompletionItem, got []protocol.CompletionItem) string {
var (
matchedIdxs []int
lastGotIdx int
inOrder = true
)
for _, w := range want {
var found bool
for i, g := range got {
if w.Label == g.Label && w.Detail == g.Detail && toProtocolCompletionItemKind(w.Kind) == g.Kind {
matchedIdxs = append(matchedIdxs, i)
found = true
if i < lastGotIdx {
inOrder = false
}
lastGotIdx = i
break
}
}
if !found {
return summarizeCompletionItems(-1, []source.CompletionItem{w}, got, "didn't find expected completion")
}
}
sort.Ints(matchedIdxs)
matched := make([]protocol.CompletionItem, 0, len(matchedIdxs))
for _, idx := range matchedIdxs {
matched = append(matched, got[idx])
}
if !inOrder {
return summarizeCompletionItems(-1, want, matched, "completions out of order")
}
return ""
}
func summarizeCompletionItems(i int, want []source.CompletionItem, got []protocol.CompletionItem, reason string, args ...interface{}) string {
msg := &bytes.Buffer{}
fmt.Fprint(msg, "completion failed")
if i >= 0 {
fmt.Fprintf(msg, " at %d", i)
}
fmt.Fprint(msg, " because of ")
fmt.Fprintf(msg, reason, args...)
fmt.Fprint(msg, ":\nexpected:\n")
for _, d := range want {
fmt.Fprintf(msg, " %v\n", d)
}
fmt.Fprintf(msg, "got:\n")
for _, d := range got {
fmt.Fprintf(msg, " %v\n", d)
}
return msg.String()
}
func (r *runner) FoldingRange(t *testing.T, data tests.FoldingRanges) { func (r *runner) FoldingRange(t *testing.T, data tests.FoldingRanges) {
for _, spn := range data { for _, spn := range data {
uri := spn.URI() uri := spn.URI()
@ -909,7 +685,7 @@ func (r *runner) diffSymbols(t *testing.T, uri span.URI, want []protocol.Documen
return "" return ""
} }
func summarizeSymbols(t *testing.T, i int, want []protocol.DocumentSymbol, got []protocol.DocumentSymbol, reason string, args ...interface{}) string { func summarizeSymbols(t *testing.T, i int, want, got []protocol.DocumentSymbol, reason string, args ...interface{}) string {
msg := &bytes.Buffer{} msg := &bytes.Buffer{}
fmt.Fprint(msg, "document symbols failed") fmt.Fprint(msg, "document symbols failed")
if i >= 0 { if i >= 0 {

View File

@ -49,7 +49,7 @@ func testSource(t *testing.T, exporter packagestest.Exporter) {
cache := cache.New() cache := cache.New()
session := cache.NewSession(ctx) session := cache.NewSession(ctx)
options := session.Options() options := tests.DefaultOptions()
options.Env = data.Config.Env options.Env = data.Config.Env
r := &runner{ r := &runner{
view: session.NewView(ctx, "source_test", span.FileURI(data.Config.Dir), options), view: session.NewView(ctx, "source_test", span.FileURI(data.Config.Dir), options),
@ -86,245 +86,194 @@ func (r *runner) Diagnostics(t *testing.T, data tests.Diagnostics) {
} }
} }
func (r *runner) Completion(t *testing.T, data tests.Completions, snippets tests.CompletionSnippets, items tests.CompletionItems) { func (r *runner) Completion(t *testing.T, data tests.Completions, items tests.CompletionItems) {
ctx := r.ctx
for src, test := range data { for src, test := range data {
var want []source.CompletionItem var want []protocol.CompletionItem
for _, pos := range test.CompletionItems { for _, pos := range test.CompletionItems {
want = append(want, *items[pos]) want = append(want, tests.ToProtocolCompletionItem(*items[pos]))
} }
f, err := r.view.GetFile(ctx, src.URI()) prefix, list := r.callCompletion(t, src, source.CompletionOptions{
if err != nil {
t.Fatalf("failed for %v: %v", src, err)
}
deepComplete := strings.Contains(string(src.URI()), "deepcomplete")
fuzzyMatch := strings.Contains(string(src.URI()), "fuzzymatch")
unimported := strings.Contains(string(src.URI()), "unimported")
list, surrounding, err := source.Completion(ctx, r.view, f.(source.GoFile), protocol.Position{
Line: float64(src.Start().Line() - 1),
Character: float64(src.Start().Column() - 1),
}, source.CompletionOptions{
Documentation: true, Documentation: true,
Deep: deepComplete, FuzzyMatching: true,
FuzzyMatching: fuzzyMatch,
Unimported: unimported,
// Crank this up so tests don't flake.
Budget: 5 * time.Second,
}) })
if err != nil { if !strings.Contains(string(src.URI()), "builtins") {
t.Fatalf("failed for %v: %v", src, err) list = tests.FilterBuiltins(list)
} }
var ( var got []protocol.CompletionItem
prefix string
fuzzyMatcher *fuzzy.Matcher
)
if surrounding != nil {
prefix = strings.ToLower(surrounding.Prefix())
if deepComplete && prefix != "" {
fuzzyMatcher = fuzzy.NewMatcher(surrounding.Prefix(), fuzzy.Symbol)
}
}
wantBuiltins := strings.Contains(string(src.URI()), "builtins")
var got []source.CompletionItem
for _, item := range list { for _, item := range list {
if !wantBuiltins && isBuiltin(item) {
continue
}
// If deep completion is enabled, we need to use the fuzzy matcher to match
// the code's behavior.
if deepComplete {
if fuzzyMatcher != nil && fuzzyMatcher.Score(item.Label) < 0 {
continue
}
} else {
// We let the client do fuzzy matching, so we return all possible candidates.
// To simplify testing, filter results with prefixes that don't match exactly.
if !strings.HasPrefix(strings.ToLower(item.Label), prefix) { if !strings.HasPrefix(strings.ToLower(item.Label), prefix) {
continue continue
} }
got = append(got, item)
}
if diff := tests.DiffCompletionItems(want, got); diff != "" {
t.Errorf("%s: %s", src, diff)
}
}
}
func (r *runner) CompletionSnippets(t *testing.T, data tests.CompletionSnippets, items tests.CompletionItems) {
for _, placeholders := range []bool{true, false} {
for src, expected := range data {
_, list := r.callCompletion(t, src, source.CompletionOptions{
Placeholders: placeholders,
Deep: true,
Budget: 5 * time.Second,
})
got := tests.FindItem(list, *items[expected.CompletionItem])
want := expected.PlainSnippet
if placeholders {
want = expected.PlaceholderSnippet
}
if diff := tests.DiffSnippets(want, got); diff != "" {
t.Errorf("%s: %s", src, diff)
}
}
}
}
func (r *runner) UnimportedCompletions(t *testing.T, data tests.UnimportedCompletions, items tests.CompletionItems) {
for src, test := range data {
var want []protocol.CompletionItem
for _, pos := range test.CompletionItems {
want = append(want, tests.ToProtocolCompletionItem(*items[pos]))
}
_, got := r.callCompletion(t, src, source.CompletionOptions{
Unimported: true,
})
if !strings.Contains(string(src.URI()), "builtins") {
got = tests.FilterBuiltins(got)
}
if diff := tests.DiffCompletionItems(want, got); diff != "" {
t.Errorf("%s: %s", src, diff)
}
}
}
func (r *runner) DeepCompletions(t *testing.T, data tests.DeepCompletions, items tests.CompletionItems) {
for src, test := range data {
var want []protocol.CompletionItem
for _, pos := range test.CompletionItems {
want = append(want, tests.ToProtocolCompletionItem(*items[pos]))
}
prefix, list := r.callCompletion(t, src, source.CompletionOptions{
Deep: true,
Budget: 5 * time.Second,
Documentation: true,
})
if !strings.Contains(string(src.URI()), "builtins") {
list = tests.FilterBuiltins(list)
}
fuzzyMatcher := fuzzy.NewMatcher(prefix, fuzzy.Symbol)
var got []protocol.CompletionItem
for _, item := range list {
if fuzzyMatcher.Score(item.Label) < 0 {
continue
} }
got = append(got, item) got = append(got, item)
} }
switch test.Type { if msg := tests.DiffCompletionItems(want, got); msg != "" {
case tests.CompletionFull:
if diff := diffCompletionItems(want, got); diff != "" {
t.Errorf("%s: %s", src, diff)
}
case tests.CompletionPartial:
if msg := checkCompletionOrder(want, got); msg != "" {
t.Errorf("%s: %s", src, msg) t.Errorf("%s: %s", src, msg)
} }
} }
} }
for _, usePlaceholders := range []bool{true, false} {
for src, want := range snippets {
f, err := r.view.GetFile(ctx, src.URI())
if err != nil {
t.Fatalf("failed for %v: %v", src, err)
}
list, _, err := source.Completion(ctx, r.view, f.(source.GoFile), protocol.Position{ func (r *runner) FuzzyCompletions(t *testing.T, data tests.FuzzyCompletions, items tests.CompletionItems) {
Line: float64(src.Start().Line() - 1), for src, test := range data {
Character: float64(src.Start().Column() - 1), var want []protocol.CompletionItem
}, source.CompletionOptions{ for _, pos := range test.CompletionItems {
Documentation: true, want = append(want, tests.ToProtocolCompletionItem(*items[pos]))
Deep: strings.Contains(string(src.URI()), "deepcomplete"), }
FuzzyMatching: strings.Contains(string(src.URI()), "fuzzymatch"), prefix, list := r.callCompletion(t, src, source.CompletionOptions{
Placeholders: usePlaceholders, FuzzyMatching: true,
// Crank this up so tests don't flake. Deep: true,
Budget: 5 * time.Second, Budget: 5 * time.Second,
}) })
if !strings.Contains(string(src.URI()), "builtins") {
list = tests.FilterBuiltins(list)
}
var fuzzyMatcher *fuzzy.Matcher
if prefix != "" {
fuzzyMatcher = fuzzy.NewMatcher(prefix, fuzzy.Symbol)
}
var got []protocol.CompletionItem
for _, item := range list {
if fuzzyMatcher != nil && fuzzyMatcher.Score(item.Label) < 0 {
continue
}
got = append(got, item)
}
if msg := tests.DiffCompletionItems(want, got); msg != "" {
t.Errorf("%s: %s", src, msg)
}
}
}
func (r *runner) RankCompletions(t *testing.T, data tests.RankCompletions, items tests.CompletionItems) {
for src, test := range data {
var want []protocol.CompletionItem
for _, pos := range test.CompletionItems {
want = append(want, tests.ToProtocolCompletionItem(*items[pos]))
}
prefix, list := r.callCompletion(t, src, source.CompletionOptions{
FuzzyMatching: true,
Deep: true,
Budget: 5 * time.Second,
})
if !strings.Contains(string(src.URI()), "builtins") {
list = tests.FilterBuiltins(list)
}
fuzzyMatcher := fuzzy.NewMatcher(prefix, fuzzy.Symbol)
var got []protocol.CompletionItem
for _, item := range list {
if fuzzyMatcher.Score(item.Label) < 0 {
continue
}
got = append(got, item)
}
if msg := tests.CheckCompletionOrder(want, got); msg != "" {
t.Errorf("%s: %s", src, msg)
}
}
}
func (r *runner) callCompletion(t *testing.T, src span.Span, options source.CompletionOptions) (string, []protocol.CompletionItem) {
f, err := r.view.GetFile(r.ctx, src.URI())
if err != nil {
t.Fatal(err)
}
list, surrounding, err := source.Completion(r.ctx, r.view, f.(source.GoFile), protocol.Position{
Line: float64(src.Start().Line() - 1),
Character: float64(src.Start().Column() - 1),
}, options)
if err != nil { if err != nil {
t.Fatalf("failed for %v: %v", src, err) t.Fatalf("failed for %v: %v", src, err)
} }
wantItem := items[want.CompletionItem] var prefix string
var got *source.CompletionItem if surrounding != nil {
for _, item := range list { prefix = strings.ToLower(surrounding.Prefix())
if item.Label == wantItem.Label {
got = &item
break
} }
} // TODO(rstambler): In testing this out, I noticed that scores are equal,
expected := want.PlainSnippet // even when they shouldn't be. This needs more investigation.
if usePlaceholders { sort.SliceStable(list, func(i, j int) bool {
expected = want.PlaceholderSnippet return list[i].Score > list[j].Score
}
if expected == "" {
if got != nil {
t.Fatalf("%s:%d: expected no matching snippet", src.URI(), src.Start().Line())
}
} else {
if got == nil {
t.Fatalf("%s:%d: couldn't find completion matching %q", src.URI(), src.Start().Line(), wantItem.Label)
}
actual := got.Snippet()
if expected != actual {
t.Errorf("%s: expected placeholder snippet %q, got %q", src, expected, actual)
}
}
}
}
}
func isBuiltin(item source.CompletionItem) bool {
// If a type has no detail, it is a builtin type.
if item.Detail == "" && item.Kind == source.TypeCompletionItem {
return true
}
// Remaining builtin constants, variables, interfaces, and functions.
trimmed := item.Label
if i := strings.Index(trimmed, "("); i >= 0 {
trimmed = trimmed[:i]
}
switch trimmed {
case "append", "cap", "close", "complex", "copy", "delete",
"error", "false", "imag", "iota", "len", "make", "new",
"nil", "panic", "print", "println", "real", "recover", "true":
return true
}
return false
}
// diffCompletionItems prints the diff between expected and actual completion
// test results.
func diffCompletionItems(want []source.CompletionItem, got []source.CompletionItem) string {
sort.SliceStable(got, func(i, j int) bool {
return got[i].Score > got[j].Score
}) })
var numDeepCompletionsSeen int
// duplicate the lsp/completion logic to limit deep candidates to keep expected var items []source.CompletionItem
// list short // Apply deep completion filtering.
var idx, seenDeepCompletions int for _, item := range list {
for _, item := range got {
if item.Depth > 0 { if item.Depth > 0 {
if seenDeepCompletions >= 3 { if !options.Deep {
continue continue
} }
seenDeepCompletions++ if numDeepCompletionsSeen >= source.MaxDeepCompletions {
continue
} }
got[idx] = item numDeepCompletionsSeen++
idx++
} }
got = got[:idx] items = append(items, item)
if len(got) != len(want) {
return summarizeCompletionItems(-1, want, got, "different lengths got %v want %v", len(got), len(want))
} }
for i, w := range want { return prefix, tests.ToProtocolCompletionItems(items)
g := got[i]
if w.Label != g.Label {
return summarizeCompletionItems(i, want, got, "incorrect Label got %v want %v", g.Label, w.Label)
}
if w.Detail != g.Detail {
return summarizeCompletionItems(i, want, got, "incorrect Detail got %v want %v", g.Detail, w.Detail)
}
if w.Documentation != "" && !strings.HasPrefix(w.Documentation, "@") {
if w.Documentation != g.Documentation {
return summarizeCompletionItems(i, want, got, "incorrect Documentation got %v want %v", g.Documentation, w.Documentation)
}
}
if w.Kind != g.Kind {
return summarizeCompletionItems(i, want, got, "incorrect Kind got %v want %v", g.Kind, w.Kind)
}
}
return ""
}
func checkCompletionOrder(want []source.CompletionItem, got []source.CompletionItem) string {
var (
matchedIdxs []int
lastGotIdx int
inOrder = true
)
for _, w := range want {
var found bool
for i, g := range got {
if w.Label == g.Label && w.Detail == g.Detail && w.Kind == g.Kind {
matchedIdxs = append(matchedIdxs, i)
found = true
if i < lastGotIdx {
inOrder = false
}
lastGotIdx = i
break
}
}
if !found {
return summarizeCompletionItems(-1, []source.CompletionItem{w}, got, "didn't find expected completion")
}
}
sort.Ints(matchedIdxs)
matched := make([]source.CompletionItem, 0, len(matchedIdxs))
for _, idx := range matchedIdxs {
matched = append(matched, got[idx])
}
if !inOrder {
return summarizeCompletionItems(-1, want, matched, "completions out of order")
}
return ""
}
func summarizeCompletionItems(i int, want []source.CompletionItem, got []source.CompletionItem, reason string, args ...interface{}) string {
msg := &bytes.Buffer{}
fmt.Fprint(msg, "completion failed")
if i >= 0 {
fmt.Fprintf(msg, " at %d", i)
}
fmt.Fprint(msg, " because of ")
fmt.Fprintf(msg, reason, args...)
fmt.Fprint(msg, ":\nexpected:\n")
for _, d := range want {
fmt.Fprintf(msg, " %v\n", d)
}
fmt.Fprintf(msg, "got:\n")
for _, d := range got {
fmt.Fprintf(msg, " %v\n", d)
}
return msg.String()
} }
func (r *runner) FoldingRange(t *testing.T, data tests.FoldingRanges) { func (r *runner) FoldingRange(t *testing.T, data tests.FoldingRanges) {

View File

@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
package deepcomplete package deep
import "context" import "context"
@ -18,7 +18,7 @@ func wantsDeepB(deepB) {}
func _() { func _() {
var a deepA //@item(deepAVar, "a", "deepA", "var") var a deepA //@item(deepAVar, "a", "deepA", "var")
a.b //@item(deepABField, "a.b", "deepB", "field") a.b //@item(deepABField, "a.b", "deepB", "field")
wantsDeepB(a) //@complete(")", deepABField, deepAVar) wantsDeepB(a) //@deep(")", deepABField, deepAVar)
deepA{a} //@snippet("}", deepABField, "a.b", "a.b") deepA{a} //@snippet("}", deepABField, "a.b", "a.b")
} }
@ -29,7 +29,7 @@ func _() {
context.Background() //@item(ctxBackground, "context.Background", "func() context.Context", "func", "Background returns a non-nil, empty Context.") context.Background() //@item(ctxBackground, "context.Background", "func() context.Context", "func", "Background returns a non-nil, empty Context.")
context.TODO() //@item(ctxTODO, "context.TODO", "func() context.Context", "func", "TODO returns a non-nil, empty Context.") context.TODO() //@item(ctxTODO, "context.TODO", "func() context.Context", "func", "TODO returns a non-nil, empty Context.")
wantsContext(c) //@completePartial(")", ctxBackground, ctxTODO) wantsContext(c) //@rank(")", ctxBackground, ctxTODO)
} }
func _() { func _() {
@ -39,7 +39,7 @@ func _() {
} }
var circle deepCircle //@item(deepCircle, "circle", "deepCircle", "var") var circle deepCircle //@item(deepCircle, "circle", "deepCircle", "var")
circle.deepCircle //@item(deepCircleField, "circle.deepCircle", "*deepCircle", "field", "deepCircle is circular.") circle.deepCircle //@item(deepCircleField, "circle.deepCircle", "*deepCircle", "field", "deepCircle is circular.")
var _ deepCircle = circ //@complete(" //", deepCircle, deepCircleField) var _ deepCircle = circ //@deep(" //", deepCircle, deepCircleField)
} }
func _() { func _() {
@ -57,7 +57,7 @@ func _() {
var a deepEmbedA //@item(deepEmbedA, "a", "deepEmbedA", "var") var a deepEmbedA //@item(deepEmbedA, "a", "deepEmbedA", "var")
a.deepEmbedB //@item(deepEmbedB, "a.deepEmbedB", "deepEmbedB", "field") a.deepEmbedB //@item(deepEmbedB, "a.deepEmbedB", "deepEmbedB", "field")
a.deepEmbedC //@item(deepEmbedC, "a.deepEmbedC", "deepEmbedC", "field") a.deepEmbedC //@item(deepEmbedC, "a.deepEmbedC", "deepEmbedC", "field")
wantsC(a) //@complete(")", deepEmbedC, deepEmbedA, deepEmbedB) wantsC(a) //@deep(")", deepEmbedC, deepEmbedA, deepEmbedB)
} }
func _() { func _() {
@ -67,7 +67,7 @@ func _() {
} }
nested{ nested{
a: 123, //@complete(" //", deepNestedField) a: 123, //@deep(" //", deepNestedField)
} }
} }
@ -86,5 +86,5 @@ func _() {
// "a.d" should be ranked above the deeper "a.b.c" // "a.d" should be ranked above the deeper "a.b.c"
var i int var i int
i = a //@complete(" //", deepAD, deepABC, deepA, deepAB) i = a //@deep(" //", deepAD, deepABC, deepA, deepAB)
} }

View File

@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
package fuzzymatch package fuzzy
func _() { func _() {
var a struct { var a struct {
@ -13,13 +13,13 @@ func _() {
a.fabar //@item(fuzzFabarField, "a.fabar", "int", "field") a.fabar //@item(fuzzFabarField, "a.fabar", "int", "field")
a.fooBar //@item(fuzzFooBarField, "a.fooBar", "string", "field") a.fooBar //@item(fuzzFooBarField, "a.fooBar", "string", "field")
afa //@complete(" //", fuzzFabarField, fuzzFooBarField) afa //@fuzzy(" //", fuzzFabarField, fuzzFooBarField)
afb //@complete(" //", fuzzFooBarField, fuzzFabarField) afb //@fuzzy(" //", fuzzFooBarField, fuzzFabarField)
fab //@complete(" //", fuzzFabarField) fab //@fuzzy(" //", fuzzFabarField)
var myString string var myString string
myString = af //@complete(" //", fuzzFooBarField, fuzzFabarField) myString = af //@fuzzy(" //", fuzzFooBarField, fuzzFabarField)
var b struct { var b struct {
c struct { c struct {
@ -40,9 +40,9 @@ func _() {
b.c.d.e.abc //@item(fuzzABCstring, "b.c.d.e.abc", "string", "field") b.c.d.e.abc //@item(fuzzABCstring, "b.c.d.e.abc", "string", "field")
// in depth order by default // in depth order by default
abc //@complete(" //", fuzzABCInt, fuzzABCbool, fuzzABCfloat) abc //@fuzzy(" //", fuzzABCInt, fuzzABCbool, fuzzABCfloat)
// deep candidate that matches expected type should still ranked first // deep candidate that matches expected type should still ranked first
var s string var s string
s = abc //@complete(" //", fuzzABCstring, fuzzABCInt, fuzzABCbool) s = abc //@fuzzy(" //", fuzzABCstring, fuzzABCInt, fuzzABCbool)
} }

View File

@ -103,7 +103,7 @@ func _() {
} }
func _() { func _() {
"func(...) {}" //@item(litFunc, "func(...) {}", "", "var") _ = "func(...) {}" //@item(litFunc, "func(...) {}", "", "var")
sort.Slice(nil, f) //@snippet(")", litFunc, "func(i, j int) bool {$0\\}", "func(i, j int) bool {$0\\}") sort.Slice(nil, f) //@snippet(")", litFunc, "func(i, j int) bool {$0\\}", "func(i, j int) bool {$0\\}")

View File

@ -91,7 +91,7 @@ func main() {
marker := strings.ReplaceAll(path, "/", "slash") marker := strings.ReplaceAll(path, "/", "slash")
markers = append(markers, marker) markers = append(markers, marker)
} }
outf(" //@complete(\"\", %s)\n", strings.Join(markers, ", ")) outf(" //@unimported(\"\", %s)\n", strings.Join(markers, ", "))
outf("}\n") outf("}\n")
outf("// Create markers for unimported std lib packages. Only for use by this test.\n") outf("// Create markers for unimported std lib packages. Only for use by this test.\n")

View File

@ -3,7 +3,7 @@
package unimported package unimported
func _() { func _() {
//@complete("", archiveslashtar, archiveslashzip, bufio, bytes, compressslashbzip2, compressslashflate, compressslashgzip, compressslashlzw, compressslashzlib, containerslashheap, containerslashlist, containerslashring, context, crypto, cryptoslashaes, cryptoslashcipher, cryptoslashdes, cryptoslashdsa, cryptoslashecdsa, cryptoslashed25519, cryptoslashelliptic, cryptoslashhmac, cryptoslashmd5, cryptoslashrand, cryptoslashrc4, cryptoslashrsa, cryptoslashsha1, cryptoslashsha256, cryptoslashsha512, cryptoslashsubtle, cryptoslashtls, cryptoslashx509, cryptoslashx509slashpkix, databaseslashsql, databaseslashsqlslashdriver, debugslashdwarf, debugslashelf, debugslashgosym, debugslashmacho, debugslashpe, debugslashplan9obj, encoding, encodingslashascii85, encodingslashasn1, encodingslashbase32, encodingslashbase64, encodingslashbinary, encodingslashcsv, encodingslashgob, encodingslashhex, encodingslashjson, encodingslashpem, encodingslashxml, errors, expvar, flag, fmt, goslashast, goslashbuild, goslashconstant, goslashdoc, goslashformat, goslashimporter, goslashparser, goslashprinter, goslashscanner, goslashtoken, goslashtypes, hash, hashslashadler32, hashslashcrc32, hashslashcrc64, hashslashfnv, html, htmlslashtemplate, image, imageslashcolor, imageslashcolorslashpalette, imageslashdraw, imageslashgif, imageslashjpeg, imageslashpng, indexslashsuffixarray, io, ioslashioutil, log, logslashsyslog, math, mathslashbig, mathslashbits, mathslashcmplx, mathslashrand, mime, mimeslashmultipart, mimeslashquotedprintable, net, netslashhttp, netslashhttpslashcgi, netslashhttpslashcookiejar, netslashhttpslashfcgi, netslashhttpslashhttptest, netslashhttpslashhttptrace, netslashhttpslashhttputil, netslashhttpslashpprof, netslashmail, netslashrpc, netslashrpcslashjsonrpc, netslashsmtp, netslashtextproto, netslashurl, os, osslashexec, osslashsignal, osslashuser, path, pathslashfilepath, plugin, reflect, regexp, regexpslashsyntax, runtime, runtimeslashdebug, runtimeslashpprof, runtimeslashtrace, sort, strconv, strings, sync, syncslashatomic, syscall, syscallslashjs, testing, testingslashiotest, testingslashquick, textslashscanner, textslashtabwriter, textslashtemplate, textslashtemplateslashparse, time, unicode, unicodeslashutf16, unicodeslashutf8, unsafe) //@unimported("", archiveslashtar, archiveslashzip, bufio, bytes, compressslashbzip2, compressslashflate, compressslashgzip, compressslashlzw, compressslashzlib, containerslashheap, containerslashlist, containerslashring, context, crypto, cryptoslashaes, cryptoslashcipher, cryptoslashdes, cryptoslashdsa, cryptoslashecdsa, cryptoslashed25519, cryptoslashelliptic, cryptoslashhmac, cryptoslashmd5, cryptoslashrand, cryptoslashrc4, cryptoslashrsa, cryptoslashsha1, cryptoslashsha256, cryptoslashsha512, cryptoslashsubtle, cryptoslashtls, cryptoslashx509, cryptoslashx509slashpkix, databaseslashsql, databaseslashsqlslashdriver, debugslashdwarf, debugslashelf, debugslashgosym, debugslashmacho, debugslashpe, debugslashplan9obj, encoding, encodingslashascii85, encodingslashasn1, encodingslashbase32, encodingslashbase64, encodingslashbinary, encodingslashcsv, encodingslashgob, encodingslashhex, encodingslashjson, encodingslashpem, encodingslashxml, errors, expvar, flag, fmt, goslashast, goslashbuild, goslashconstant, goslashdoc, goslashformat, goslashimporter, goslashparser, goslashprinter, goslashscanner, goslashtoken, goslashtypes, hash, hashslashadler32, hashslashcrc32, hashslashcrc64, hashslashfnv, html, htmlslashtemplate, image, imageslashcolor, imageslashcolorslashpalette, imageslashdraw, imageslashgif, imageslashjpeg, imageslashpng, indexslashsuffixarray, io, ioslashioutil, log, logslashsyslog, math, mathslashbig, mathslashbits, mathslashcmplx, mathslashrand, mime, mimeslashmultipart, mimeslashquotedprintable, net, netslashhttp, netslashhttpslashcgi, netslashhttpslashcookiejar, netslashhttpslashfcgi, netslashhttpslashhttptest, netslashhttpslashhttptrace, netslashhttpslashhttputil, netslashhttpslashpprof, netslashmail, netslashrpc, netslashrpcslashjsonrpc, netslashsmtp, netslashtextproto, netslashurl, os, osslashexec, osslashsignal, osslashuser, path, pathslashfilepath, plugin, reflect, regexp, regexpslashsyntax, runtime, runtimeslashdebug, runtimeslashpprof, runtimeslashtrace, sort, strconv, strings, sync, syncslashatomic, syscall, syscallslashjs, testing, testingslashiotest, testingslashquick, textslashscanner, textslashtabwriter, textslashtemplate, textslashtemplateslashparse, time, unicode, unicodeslashutf16, unicodeslashutf8, unsafe)
} }
// Create markers for unimported std lib packages. Only for use by this test. // Create markers for unimported std lib packages. Only for use by this test.

View File

@ -0,0 +1,193 @@
package tests
import (
"bytes"
"fmt"
"sort"
"strings"
"golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/lsp/source"
)
func ToProtocolCompletionItems(items []source.CompletionItem) []protocol.CompletionItem {
var result []protocol.CompletionItem
for _, item := range items {
result = append(result, ToProtocolCompletionItem(item))
}
return result
}
func ToProtocolCompletionItem(item source.CompletionItem) protocol.CompletionItem {
return protocol.CompletionItem{
Label: item.Label,
Kind: toProtocolCompletionItemKind(item.Kind),
Detail: item.Detail,
Documentation: item.Documentation,
InsertText: item.InsertText,
TextEdit: &protocol.TextEdit{
NewText: item.Snippet(),
},
}
}
func toProtocolCompletionItemKind(kind source.CompletionItemKind) protocol.CompletionItemKind {
switch kind {
case source.InterfaceCompletionItem:
return protocol.InterfaceCompletion
case source.StructCompletionItem:
return protocol.StructCompletion
case source.TypeCompletionItem:
return protocol.TypeParameterCompletion // ??
case source.ConstantCompletionItem:
return protocol.ConstantCompletion
case source.FieldCompletionItem:
return protocol.FieldCompletion
case source.ParameterCompletionItem, source.VariableCompletionItem:
return protocol.VariableCompletion
case source.FunctionCompletionItem:
return protocol.FunctionCompletion
case source.MethodCompletionItem:
return protocol.MethodCompletion
case source.PackageCompletionItem:
return protocol.ModuleCompletion // ??
default:
return protocol.TextCompletion
}
}
func FilterBuiltins(items []protocol.CompletionItem) []protocol.CompletionItem {
var got []protocol.CompletionItem
for _, item := range items {
if isBuiltin(item.Label, item.Detail, item.Kind) {
continue
}
got = append(got, item)
}
return got
}
func isBuiltin(label, detail string, kind protocol.CompletionItemKind) bool {
if detail == "" && kind == protocol.TypeParameterCompletion {
return true
}
// Remaining builtin constants, variables, interfaces, and functions.
trimmed := label
if i := strings.Index(trimmed, "("); i >= 0 {
trimmed = trimmed[:i]
}
switch trimmed {
case "append", "cap", "close", "complex", "copy", "delete",
"error", "false", "imag", "iota", "len", "make", "new",
"nil", "panic", "print", "println", "real", "recover", "true":
return true
}
return false
}
func CheckCompletionOrder(want, got []protocol.CompletionItem) string {
var (
matchedIdxs []int
lastGotIdx int
inOrder = true
)
for _, w := range want {
var found bool
for i, g := range got {
if w.Label == g.Label && w.Detail == g.Detail && w.Kind == g.Kind {
matchedIdxs = append(matchedIdxs, i)
found = true
if i < lastGotIdx {
inOrder = false
}
lastGotIdx = i
break
}
}
if !found {
return summarizeCompletionItems(-1, []protocol.CompletionItem{w}, got, "didn't find expected completion")
}
}
sort.Ints(matchedIdxs)
matched := make([]protocol.CompletionItem, 0, len(matchedIdxs))
for _, idx := range matchedIdxs {
matched = append(matched, got[idx])
}
if !inOrder {
return summarizeCompletionItems(-1, want, matched, "completions out of order")
}
return ""
}
func DiffSnippets(want string, got *protocol.CompletionItem) string {
if want == "" {
if got != nil {
return fmt.Sprintf("expected no snippet but got %s", got.TextEdit.NewText)
}
} else {
if got == nil {
return fmt.Sprintf("couldn't find completion matching %q", want)
}
if want != got.TextEdit.NewText {
return fmt.Sprintf("expected snippet %q, got %q", want, got.TextEdit.NewText)
}
}
return ""
}
func FindItem(list []protocol.CompletionItem, want source.CompletionItem) *protocol.CompletionItem {
for _, item := range list {
if item.Label == want.Label {
return &item
}
}
return nil
}
// DiffCompletionItems prints the diff between expected and actual completion
// test results.
func DiffCompletionItems(want, got []protocol.CompletionItem) string {
if len(got) != len(want) {
return summarizeCompletionItems(-1, want, got, "different lengths got %v want %v", len(got), len(want))
}
for i, w := range want {
g := got[i]
if w.Label != g.Label {
return summarizeCompletionItems(i, want, got, "incorrect Label got %v want %v", g.Label, w.Label)
}
if w.Detail != g.Detail {
return summarizeCompletionItems(i, want, got, "incorrect Detail got %v want %v", g.Detail, w.Detail)
}
if w.Documentation != "" && !strings.HasPrefix(w.Documentation, "@") {
if w.Documentation != g.Documentation {
return summarizeCompletionItems(i, want, got, "incorrect Documentation got %v want %v", g.Documentation, w.Documentation)
}
}
if w.Kind != g.Kind {
return summarizeCompletionItems(i, want, got, "incorrect Kind got %v want %v", g.Kind, w.Kind)
}
}
return ""
}
func summarizeCompletionItems(i int, want, got []protocol.CompletionItem, reason string, args ...interface{}) string {
msg := &bytes.Buffer{}
fmt.Fprint(msg, "completion failed")
if i >= 0 {
fmt.Fprintf(msg, " at %d", i)
}
fmt.Fprint(msg, " because of ")
fmt.Fprintf(msg, reason, args...)
fmt.Fprint(msg, ":\nexpected:\n")
for _, d := range want {
fmt.Fprintf(msg, " %v\n", d)
}
fmt.Fprintf(msg, "got:\n")
for _, d := range got {
fmt.Fprintf(msg, " %v\n", d)
}
return msg.String()
}

View File

@ -0,0 +1,91 @@
package tests
import (
"bytes"
"fmt"
"sort"
"strings"
"golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/lsp/source"
"golang.org/x/tools/internal/span"
)
// DiffDiagnostics prints the diff between expected and actual diagnostics test
// results.
func DiffDiagnostics(uri span.URI, want, got []source.Diagnostic) string {
sortDiagnostics(want)
sortDiagnostics(got)
if len(got) != len(want) {
return summarizeDiagnostics(-1, want, got, "different lengths got %v want %v", len(got), len(want))
}
for i, w := range want {
g := got[i]
if w.Message != g.Message {
return summarizeDiagnostics(i, want, got, "incorrect Message got %v want %v", g.Message, w.Message)
}
if protocol.ComparePosition(w.Range.Start, g.Range.Start) != 0 {
return summarizeDiagnostics(i, want, got, "incorrect Start got %v want %v", g.Range.Start, w.Range.Start)
}
// Special case for diagnostics on parse errors.
if strings.Contains(string(uri), "noparse") {
if protocol.ComparePosition(g.Range.Start, g.Range.End) != 0 || protocol.ComparePosition(w.Range.Start, g.Range.End) != 0 {
return summarizeDiagnostics(i, want, got, "incorrect End got %v want %v", g.Range.End, w.Range.Start)
}
} else if !protocol.IsPoint(g.Range) { // Accept any 'want' range if the diagnostic returns a zero-length range.
if protocol.ComparePosition(w.Range.End, g.Range.End) != 0 {
return summarizeDiagnostics(i, want, got, "incorrect End got %v want %v", g.Range.End, w.Range.End)
}
}
if w.Severity != g.Severity {
return summarizeDiagnostics(i, want, got, "incorrect Severity got %v want %v", g.Severity, w.Severity)
}
if w.Source != g.Source {
return summarizeDiagnostics(i, want, got, "incorrect Source got %v want %v", g.Source, w.Source)
}
}
return ""
}
func sortDiagnostics(d []source.Diagnostic) {
sort.Slice(d, func(i int, j int) bool {
return compareDiagnostic(d[i], d[j]) < 0
})
}
func compareDiagnostic(a, b source.Diagnostic) int {
if r := span.CompareURI(a.URI, b.URI); r != 0 {
return r
}
if r := protocol.CompareRange(a.Range, b.Range); r != 0 {
return r
}
if a.Message < b.Message {
return -1
}
if a.Message == b.Message {
return 0
} else {
return 1
}
}
func summarizeDiagnostics(i int, want []source.Diagnostic, got []source.Diagnostic, reason string, args ...interface{}) string {
msg := &bytes.Buffer{}
fmt.Fprint(msg, "diagnostics failed")
if i >= 0 {
fmt.Fprintf(msg, " at %d", i)
}
fmt.Fprint(msg, " because of ")
fmt.Fprintf(msg, reason, args...)
fmt.Fprint(msg, ":\nexpected:\n")
for _, d := range want {
fmt.Fprintf(msg, " %s:%v: %s\n", d.URI, d.Range, d.Message)
}
fmt.Fprintf(msg, "got:\n")
for _, d := range got {
fmt.Fprintf(msg, " %s:%v: %s\n", d.URI, d.Range, d.Message)
}
return msg.String()
}

View File

@ -2,13 +2,12 @@
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
// Package tests exports functionality to be used across a variety of gopls tests.
package tests package tests
import ( import (
"bytes"
"context" "context"
"flag" "flag"
"fmt"
"go/ast" "go/ast"
"go/token" "go/token"
"io/ioutil" "io/ioutil"
@ -30,8 +29,12 @@ import (
// We hardcode the expected number of test cases to ensure that all tests // We hardcode the expected number of test cases to ensure that all tests
// are being executed. If a test is added, this number must be changed. // are being executed. If a test is added, this number must be changed.
const ( const (
ExpectedCompletionsCount = 165 ExpectedCompletionsCount = 152
ExpectedCompletionSnippetCount = 35 ExpectedCompletionSnippetCount = 35
ExpectedUnimportedCompletionsCount = 1
ExpectedDeepCompletionsCount = 5
ExpectedFuzzyCompletionsCount = 6
ExpectedRankedCompletionsCount = 1
ExpectedDiagnosticsCount = 21 ExpectedDiagnosticsCount = 21
ExpectedFormatCount = 6 ExpectedFormatCount = 6
ExpectedImportCount = 2 ExpectedImportCount = 2
@ -61,6 +64,10 @@ type Diagnostics map[span.URI][]source.Diagnostic
type CompletionItems map[token.Pos]*source.CompletionItem type CompletionItems map[token.Pos]*source.CompletionItem
type Completions map[span.Span]Completion type Completions map[span.Span]Completion
type CompletionSnippets map[span.Span]CompletionSnippet type CompletionSnippets map[span.Span]CompletionSnippet
type UnimportedCompletions map[span.Span]Completion
type DeepCompletions map[span.Span]Completion
type FuzzyCompletions map[span.Span]Completion
type RankCompletions map[span.Span]Completion
type FoldingRanges []span.Span type FoldingRanges []span.Span
type Formats []span.Span type Formats []span.Span
type Imports []span.Span type Imports []span.Span
@ -82,6 +89,10 @@ type Data struct {
CompletionItems CompletionItems CompletionItems CompletionItems
Completions Completions Completions Completions
CompletionSnippets CompletionSnippets CompletionSnippets CompletionSnippets
UnimportedCompletions UnimportedCompletions
DeepCompletions DeepCompletions
FuzzyCompletions FuzzyCompletions
RankCompletions RankCompletions
FoldingRanges FoldingRanges FoldingRanges FoldingRanges
Formats Formats Formats Formats
Imports Imports Imports Imports
@ -107,7 +118,12 @@ type Data struct {
type Tests interface { type Tests interface {
Diagnostics(*testing.T, Diagnostics) Diagnostics(*testing.T, Diagnostics)
Completion(*testing.T, Completions, CompletionSnippets, CompletionItems) Completion(*testing.T, Completions, CompletionItems)
CompletionSnippets(*testing.T, CompletionSnippets, CompletionItems)
UnimportedCompletions(*testing.T, UnimportedCompletions, CompletionItems)
DeepCompletions(*testing.T, DeepCompletions, CompletionItems)
FuzzyCompletions(*testing.T, FuzzyCompletions, CompletionItems)
RankCompletions(*testing.T, RankCompletions, CompletionItems)
FoldingRange(*testing.T, FoldingRanges) FoldingRange(*testing.T, FoldingRanges)
Format(*testing.T, Formats) Format(*testing.T, Formats)
Import(*testing.T, Imports) Import(*testing.T, Imports)
@ -132,16 +148,24 @@ type Definition struct {
type CompletionTestType int type CompletionTestType int
const ( const (
// Full means candidates in test must match full list of candidates. // Default runs the standard completion tests.
CompletionFull CompletionTestType = iota CompletionDefault = CompletionTestType(iota)
// Partial means candidates in test must be valid and in the right relative order. // Unimported tests the autocompletion of unimported packages.
CompletionPartial CompletionUnimported
// Deep tests deep completion.
CompletionDeep
// Fuzzy tests deep completion and fuzzy matching.
CompletionFuzzy
// CompletionRank candidates in test must be valid and in the right relative order.
CompletionRank
) )
type Completion struct { type Completion struct {
CompletionItems []token.Pos CompletionItems []token.Pos
Type CompletionTestType
} }
type CompletionSnippet struct { type CompletionSnippet struct {
@ -166,6 +190,21 @@ func Context(t testing.TB) context.Context {
return context.Background() return context.Background()
} }
func DefaultOptions() source.Options {
o := source.DefaultOptions
o.SupportedCodeActions = map[source.FileKind]map[protocol.CodeActionKind]bool{
source.Go: {
protocol.SourceOrganizeImports: true,
protocol.QuickFix: true,
},
source.Mod: {},
source.Sum: {},
}
o.HoverKind = source.SynopsisDocumentation
o.InsertTextFormat = protocol.SnippetTextFormat
return o
}
func Load(t testing.TB, exporter packagestest.Exporter, dir string) *Data { func Load(t testing.TB, exporter packagestest.Exporter, dir string) *Data {
t.Helper() t.Helper()
@ -174,6 +213,10 @@ func Load(t testing.TB, exporter packagestest.Exporter, dir string) *Data {
CompletionItems: make(CompletionItems), CompletionItems: make(CompletionItems),
Completions: make(Completions), Completions: make(Completions),
CompletionSnippets: make(CompletionSnippets), CompletionSnippets: make(CompletionSnippets),
UnimportedCompletions: make(UnimportedCompletions),
DeepCompletions: make(DeepCompletions),
FuzzyCompletions: make(FuzzyCompletions),
RankCompletions: make(RankCompletions),
Definitions: make(Definitions), Definitions: make(Definitions),
Highlights: make(Highlights), Highlights: make(Highlights),
References: make(References), References: make(References),
@ -226,7 +269,7 @@ func Load(t testing.TB, exporter packagestest.Exporter, dir string) *Data {
}, },
} }
data.Exported = packagestest.Export(t, exporter, modules) data.Exported = packagestest.Export(t, exporter, modules)
for fragment, _ := range files { for fragment := range files {
filename := data.Exported.File(testModule, fragment) filename := data.Exported.File(testModule, fragment)
data.fragments[filename] = fragment data.fragments[filename] = fragment
} }
@ -254,8 +297,12 @@ func Load(t testing.TB, exporter packagestest.Exporter, dir string) *Data {
if err := data.Exported.Expect(map[string]interface{}{ if err := data.Exported.Expect(map[string]interface{}{
"diag": data.collectDiagnostics, "diag": data.collectDiagnostics,
"item": data.collectCompletionItems, "item": data.collectCompletionItems,
"complete": data.collectCompletions(CompletionFull), "complete": data.collectCompletions(CompletionDefault),
"completePartial": data.collectCompletions(CompletionPartial), "unimported": data.collectCompletions(CompletionUnimported),
"deep": data.collectCompletions(CompletionDeep),
"fuzzy": data.collectCompletions(CompletionFuzzy),
"rank": data.collectCompletions(CompletionRank),
"snippet": data.collectCompletionSnippets,
"fold": data.collectFoldingRanges, "fold": data.collectFoldingRanges,
"format": data.collectFormats, "format": data.collectFormats,
"import": data.collectImports, "import": data.collectImports,
@ -268,7 +315,8 @@ func Load(t testing.TB, exporter packagestest.Exporter, dir string) *Data {
"prepare": data.collectPrepareRenames, "prepare": data.collectPrepareRenames,
"symbol": data.collectSymbols, "symbol": data.collectSymbols,
"signature": data.collectSignatures, "signature": data.collectSignatures,
"snippet": data.collectCompletionSnippets,
// LSP-only features.
"link": data.collectLinks, "link": data.collectLinks,
"suggestedfix": data.collectSuggestedFixes, "suggestedfix": data.collectSuggestedFixes,
}); err != nil { }); err != nil {
@ -292,15 +340,56 @@ func Load(t testing.TB, exporter packagestest.Exporter, dir string) *Data {
func Run(t *testing.T, tests Tests, data *Data) { func Run(t *testing.T, tests Tests, data *Data) {
t.Helper() t.Helper()
t.Run("Completion", func(t *testing.T) { t.Run("Completion", func(t *testing.T) {
t.Helper() t.Helper()
if len(data.Completions) != ExpectedCompletionsCount { if len(data.Completions) != ExpectedCompletionsCount {
t.Errorf("got %v completions expected %v", len(data.Completions), ExpectedCompletionsCount) t.Errorf("got %v completions expected %v", len(data.Completions), ExpectedCompletionsCount)
} }
tests.Completion(t, data.Completions, data.CompletionItems)
})
t.Run("CompletionSnippets", func(t *testing.T) {
t.Helper()
if len(data.CompletionSnippets) != ExpectedCompletionSnippetCount { if len(data.CompletionSnippets) != ExpectedCompletionSnippetCount {
t.Errorf("got %v snippets expected %v", len(data.CompletionSnippets), ExpectedCompletionSnippetCount) t.Errorf("got %v snippets expected %v", len(data.CompletionSnippets), ExpectedCompletionSnippetCount)
} }
tests.Completion(t, data.Completions, data.CompletionSnippets, data.CompletionItems) if len(data.CompletionSnippets) != ExpectedCompletionSnippetCount {
t.Errorf("got %v snippets expected %v", len(data.CompletionSnippets), ExpectedCompletionSnippetCount)
}
tests.CompletionSnippets(t, data.CompletionSnippets, data.CompletionItems)
})
t.Run("UnimportedCompletion", func(t *testing.T) {
t.Helper()
if len(data.UnimportedCompletions) != ExpectedUnimportedCompletionsCount {
t.Errorf("got %v unimported completions expected %v", len(data.UnimportedCompletions), ExpectedUnimportedCompletionsCount)
}
tests.UnimportedCompletions(t, data.UnimportedCompletions, data.CompletionItems)
})
t.Run("DeepCompletion", func(t *testing.T) {
t.Helper()
if len(data.DeepCompletions) != ExpectedDeepCompletionsCount {
t.Errorf("got %v deep completions expected %v", len(data.DeepCompletions), ExpectedDeepCompletionsCount)
}
tests.DeepCompletions(t, data.DeepCompletions, data.CompletionItems)
})
t.Run("FuzzyCompletion", func(t *testing.T) {
t.Helper()
if len(data.FuzzyCompletions) != ExpectedFuzzyCompletionsCount {
t.Errorf("got %v fuzzy completions expected %v", len(data.FuzzyCompletions), ExpectedFuzzyCompletionsCount)
}
tests.FuzzyCompletions(t, data.FuzzyCompletions, data.CompletionItems)
})
t.Run("RankCompletions", func(t *testing.T) {
t.Helper()
if len(data.RankCompletions) != ExpectedRankedCompletionsCount {
t.Errorf("got %v fuzzy completions expected %v", len(data.RankCompletions), ExpectedRankedCompletionsCount)
}
tests.RankCompletions(t, data.RankCompletions, data.CompletionItems)
}) })
t.Run("Diagnostics", func(t *testing.T) { t.Run("Diagnostics", func(t *testing.T) {
@ -528,90 +617,32 @@ func (data *Data) collectDiagnostics(spn span.Span, msgSource, msg string) {
data.Diagnostics[spn.URI()] = append(data.Diagnostics[spn.URI()], want) data.Diagnostics[spn.URI()] = append(data.Diagnostics[spn.URI()], want)
} }
// diffDiagnostics prints the diff between expected and actual diagnostics test
// results.
func DiffDiagnostics(uri span.URI, want, got []source.Diagnostic) string {
sortDiagnostics(want)
sortDiagnostics(got)
if len(got) != len(want) {
return summarizeDiagnostics(-1, want, got, "different lengths got %v want %v", len(got), len(want))
}
for i, w := range want {
g := got[i]
if w.Message != g.Message {
return summarizeDiagnostics(i, want, got, "incorrect Message got %v want %v", g.Message, w.Message)
}
if protocol.ComparePosition(w.Range.Start, g.Range.Start) != 0 {
return summarizeDiagnostics(i, want, got, "incorrect Start got %v want %v", g.Range.Start, w.Range.Start)
}
// Special case for diagnostics on parse errors.
if strings.Contains(string(uri), "noparse") {
if protocol.ComparePosition(g.Range.Start, g.Range.End) != 0 || protocol.ComparePosition(w.Range.Start, g.Range.End) != 0 {
return summarizeDiagnostics(i, want, got, "incorrect End got %v want %v", g.Range.End, w.Range.Start)
}
} else if !protocol.IsPoint(g.Range) { // Accept any 'want' range if the diagnostic returns a zero-length range.
if protocol.ComparePosition(w.Range.End, g.Range.End) != 0 {
return summarizeDiagnostics(i, want, got, "incorrect End got %v want %v", g.Range.End, w.Range.End)
}
}
if w.Severity != g.Severity {
return summarizeDiagnostics(i, want, got, "incorrect Severity got %v want %v", g.Severity, w.Severity)
}
if w.Source != g.Source {
return summarizeDiagnostics(i, want, got, "incorrect Source got %v want %v", g.Source, w.Source)
}
}
return ""
}
func sortDiagnostics(d []source.Diagnostic) {
sort.Slice(d, func(i int, j int) bool {
return compareDiagnostic(d[i], d[j]) < 0
})
}
func compareDiagnostic(a, b source.Diagnostic) int {
if r := span.CompareURI(a.URI, b.URI); r != 0 {
return r
}
if r := protocol.CompareRange(a.Range, b.Range); r != 0 {
return r
}
if a.Message < b.Message {
return -1
}
if a.Message == b.Message {
return 0
} else {
return 1
}
}
func summarizeDiagnostics(i int, want []source.Diagnostic, got []source.Diagnostic, reason string, args ...interface{}) string {
msg := &bytes.Buffer{}
fmt.Fprint(msg, "diagnostics failed")
if i >= 0 {
fmt.Fprintf(msg, " at %d", i)
}
fmt.Fprint(msg, " because of ")
fmt.Fprintf(msg, reason, args...)
fmt.Fprint(msg, ":\nexpected:\n")
for _, d := range want {
fmt.Fprintf(msg, " %s:%v: %s\n", d.URI, d.Range, d.Message)
}
fmt.Fprintf(msg, "got:\n")
for _, d := range got {
fmt.Fprintf(msg, " %s:%v: %s\n", d.URI, d.Range, d.Message)
}
return msg.String()
}
func (data *Data) collectCompletions(typ CompletionTestType) func(span.Span, []token.Pos) { func (data *Data) collectCompletions(typ CompletionTestType) func(span.Span, []token.Pos) {
return func(src span.Span, expected []token.Pos) { result := func(m map[span.Span]Completion, src span.Span, expected []token.Pos) {
data.Completions[src] = Completion{ m[src] = Completion{
CompletionItems: expected, CompletionItems: expected,
Type: typ, }
}
switch typ {
case CompletionDeep:
return func(src span.Span, expected []token.Pos) {
result(data.DeepCompletions, src, expected)
}
case CompletionUnimported:
return func(src span.Span, expected []token.Pos) {
result(data.UnimportedCompletions, src, expected)
}
case CompletionFuzzy:
return func(src span.Span, expected []token.Pos) {
result(data.FuzzyCompletions, src, expected)
}
case CompletionRank:
return func(src span.Span, expected []token.Pos) {
result(data.RankCompletions, src, expected)
}
default:
return func(src span.Span, expected []token.Pos) {
result(data.Completions, src, expected)
} }
} }
} }