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
}

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
// client fetches updated completions after every key stroke.
IsIncomplete: options.Completion.Deep,
Items: s.toProtocolCompletionItems(candidates, rng, options),
Items: toProtocolCompletionItems(candidates, rng, options),
}, 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 (
items = make([]protocol.CompletionItem, 0, len(candidates))
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"
"strings"
"testing"
"time"
"golang.org/x/tools/go/packages/packagestest"
"golang.org/x/tools/internal/lsp/cache"
@ -51,18 +50,7 @@ func testLSP(t *testing.T, exporter packagestest.Exporter) {
cache := cache.New()
session := cache.NewSession(ctx)
options := session.Options()
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
options := tests.DefaultOptions()
session.SetOptions(options)
options.Env = data.Config.Env
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) {
for _, spn := range data {
uri := spn.URI()
@ -909,7 +685,7 @@ func (r *runner) diffSymbols(t *testing.T, uri span.URI, want []protocol.Documen
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{}
fmt.Fprint(msg, "document symbols failed")
if i >= 0 {

View File

@ -49,7 +49,7 @@ func testSource(t *testing.T, exporter packagestest.Exporter) {
cache := cache.New()
session := cache.NewSession(ctx)
options := session.Options()
options := tests.DefaultOptions()
options.Env = data.Config.Env
r := &runner{
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) {
ctx := r.ctx
func (r *runner) Completion(t *testing.T, data tests.Completions, items tests.CompletionItems) {
for src, test := range data {
var want []source.CompletionItem
var want []protocol.CompletionItem
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())
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{
prefix, list := r.callCompletion(t, src, source.CompletionOptions{
Documentation: true,
Deep: deepComplete,
FuzzyMatching: fuzzyMatch,
Unimported: unimported,
// Crank this up so tests don't flake.
Budget: 5 * time.Second,
FuzzyMatching: true,
})
if err != nil {
t.Fatalf("failed for %v: %v", src, err)
if !strings.Contains(string(src.URI()), "builtins") {
list = tests.FilterBuiltins(list)
}
var (
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
var got []protocol.CompletionItem
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) {
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)
}
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 != "" {
if msg := tests.DiffCompletionItems(want, got); 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{
Line: float64(src.Start().Line() - 1),
Character: float64(src.Start().Column() - 1),
}, source.CompletionOptions{
Documentation: true,
Deep: strings.Contains(string(src.URI()), "deepcomplete"),
FuzzyMatching: strings.Contains(string(src.URI()), "fuzzymatch"),
Placeholders: usePlaceholders,
// Crank this up so tests don't flake.
func (r *runner) FuzzyCompletions(t *testing.T, data tests.FuzzyCompletions, 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)
}
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 {
t.Fatalf("failed for %v: %v", src, err)
}
wantItem := items[want.CompletionItem]
var got *source.CompletionItem
for _, item := range list {
if item.Label == wantItem.Label {
got = &item
break
var prefix string
if surrounding != nil {
prefix = strings.ToLower(surrounding.Prefix())
}
}
expected := want.PlainSnippet
if usePlaceholders {
expected = want.PlaceholderSnippet
}
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
// TODO(rstambler): In testing this out, I noticed that scores are equal,
// even when they shouldn't be. This needs more investigation.
sort.SliceStable(list, func(i, j int) bool {
return list[i].Score > list[j].Score
})
// duplicate the lsp/completion logic to limit deep candidates to keep expected
// list short
var idx, seenDeepCompletions int
for _, item := range got {
var numDeepCompletionsSeen int
var items []source.CompletionItem
// Apply deep completion filtering.
for _, item := range list {
if item.Depth > 0 {
if seenDeepCompletions >= 3 {
if !options.Deep {
continue
}
seenDeepCompletions++
if numDeepCompletionsSeen >= source.MaxDeepCompletions {
continue
}
got[idx] = item
idx++
numDeepCompletionsSeen++
}
got = got[:idx]
if len(got) != len(want) {
return summarizeCompletionItems(-1, want, got, "different lengths got %v want %v", len(got), len(want))
items = append(items, item)
}
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 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()
return prefix, tests.ToProtocolCompletionItems(items)
}
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
// license that can be found in the LICENSE file.
package deepcomplete
package deep
import "context"
@ -18,7 +18,7 @@ func wantsDeepB(deepB) {}
func _() {
var a deepA //@item(deepAVar, "a", "deepA", "var")
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")
}
@ -29,7 +29,7 @@ func _() {
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.")
wantsContext(c) //@completePartial(")", ctxBackground, ctxTODO)
wantsContext(c) //@rank(")", ctxBackground, ctxTODO)
}
func _() {
@ -39,7 +39,7 @@ func _() {
}
var circle deepCircle //@item(deepCircle, "circle", "deepCircle", "var")
circle.deepCircle //@item(deepCircleField, "circle.deepCircle", "*deepCircle", "field", "deepCircle is circular.")
var _ deepCircle = circ //@complete(" //", deepCircle, deepCircleField)
var _ deepCircle = circ //@deep(" //", deepCircle, deepCircleField)
}
func _() {
@ -57,7 +57,7 @@ func _() {
var a deepEmbedA //@item(deepEmbedA, "a", "deepEmbedA", "var")
a.deepEmbedB //@item(deepEmbedB, "a.deepEmbedB", "deepEmbedB", "field")
a.deepEmbedC //@item(deepEmbedC, "a.deepEmbedC", "deepEmbedC", "field")
wantsC(a) //@complete(")", deepEmbedC, deepEmbedA, deepEmbedB)
wantsC(a) //@deep(")", deepEmbedC, deepEmbedA, deepEmbedB)
}
func _() {
@ -67,7 +67,7 @@ func _() {
}
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"
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
// license that can be found in the LICENSE file.
package fuzzymatch
package fuzzy
func _() {
var a struct {
@ -13,13 +13,13 @@ func _() {
a.fabar //@item(fuzzFabarField, "a.fabar", "int", "field")
a.fooBar //@item(fuzzFooBarField, "a.fooBar", "string", "field")
afa //@complete(" //", fuzzFabarField, fuzzFooBarField)
afb //@complete(" //", fuzzFooBarField, fuzzFabarField)
afa //@fuzzy(" //", fuzzFabarField, fuzzFooBarField)
afb //@fuzzy(" //", fuzzFooBarField, fuzzFabarField)
fab //@complete(" //", fuzzFabarField)
fab //@fuzzy(" //", fuzzFabarField)
var myString string
myString = af //@complete(" //", fuzzFooBarField, fuzzFabarField)
myString = af //@fuzzy(" //", fuzzFooBarField, fuzzFabarField)
var b struct {
c struct {
@ -40,9 +40,9 @@ func _() {
b.c.d.e.abc //@item(fuzzABCstring, "b.c.d.e.abc", "string", "field")
// 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
var s string
s = abc //@complete(" //", fuzzABCstring, fuzzABCInt, fuzzABCbool)
s = abc //@fuzzy(" //", fuzzABCstring, fuzzABCInt, fuzzABCbool)
}

View File

@ -103,7 +103,7 @@ 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\\}")

View File

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

View File

@ -3,7 +3,7 @@
package unimported
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.

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
// license that can be found in the LICENSE file.
// Package tests exports functionality to be used across a variety of gopls tests.
package tests
import (
"bytes"
"context"
"flag"
"fmt"
"go/ast"
"go/token"
"io/ioutil"
@ -30,8 +29,12 @@ import (
// 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.
const (
ExpectedCompletionsCount = 165
ExpectedCompletionsCount = 152
ExpectedCompletionSnippetCount = 35
ExpectedUnimportedCompletionsCount = 1
ExpectedDeepCompletionsCount = 5
ExpectedFuzzyCompletionsCount = 6
ExpectedRankedCompletionsCount = 1
ExpectedDiagnosticsCount = 21
ExpectedFormatCount = 6
ExpectedImportCount = 2
@ -61,6 +64,10 @@ type Diagnostics map[span.URI][]source.Diagnostic
type CompletionItems map[token.Pos]*source.CompletionItem
type Completions map[span.Span]Completion
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 Formats []span.Span
type Imports []span.Span
@ -82,6 +89,10 @@ type Data struct {
CompletionItems CompletionItems
Completions Completions
CompletionSnippets CompletionSnippets
UnimportedCompletions UnimportedCompletions
DeepCompletions DeepCompletions
FuzzyCompletions FuzzyCompletions
RankCompletions RankCompletions
FoldingRanges FoldingRanges
Formats Formats
Imports Imports
@ -107,7 +118,12 @@ type Data struct {
type Tests interface {
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)
Format(*testing.T, Formats)
Import(*testing.T, Imports)
@ -132,16 +148,24 @@ type Definition struct {
type CompletionTestType int
const (
// Full means candidates in test must match full list of candidates.
CompletionFull CompletionTestType = iota
// Default runs the standard completion tests.
CompletionDefault = CompletionTestType(iota)
// Partial means candidates in test must be valid and in the right relative order.
CompletionPartial
// Unimported tests the autocompletion of unimported packages.
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 {
CompletionItems []token.Pos
Type CompletionTestType
}
type CompletionSnippet struct {
@ -166,6 +190,21 @@ func Context(t testing.TB) context.Context {
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 {
t.Helper()
@ -174,6 +213,10 @@ func Load(t testing.TB, exporter packagestest.Exporter, dir string) *Data {
CompletionItems: make(CompletionItems),
Completions: make(Completions),
CompletionSnippets: make(CompletionSnippets),
UnimportedCompletions: make(UnimportedCompletions),
DeepCompletions: make(DeepCompletions),
FuzzyCompletions: make(FuzzyCompletions),
RankCompletions: make(RankCompletions),
Definitions: make(Definitions),
Highlights: make(Highlights),
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)
for fragment, _ := range files {
for fragment := range files {
filename := data.Exported.File(testModule, 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{}{
"diag": data.collectDiagnostics,
"item": data.collectCompletionItems,
"complete": data.collectCompletions(CompletionFull),
"completePartial": data.collectCompletions(CompletionPartial),
"complete": data.collectCompletions(CompletionDefault),
"unimported": data.collectCompletions(CompletionUnimported),
"deep": data.collectCompletions(CompletionDeep),
"fuzzy": data.collectCompletions(CompletionFuzzy),
"rank": data.collectCompletions(CompletionRank),
"snippet": data.collectCompletionSnippets,
"fold": data.collectFoldingRanges,
"format": data.collectFormats,
"import": data.collectImports,
@ -268,7 +315,8 @@ func Load(t testing.TB, exporter packagestest.Exporter, dir string) *Data {
"prepare": data.collectPrepareRenames,
"symbol": data.collectSymbols,
"signature": data.collectSignatures,
"snippet": data.collectCompletionSnippets,
// LSP-only features.
"link": data.collectLinks,
"suggestedfix": data.collectSuggestedFixes,
}); 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) {
t.Helper()
t.Run("Completion", func(t *testing.T) {
t.Helper()
if 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 {
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) {
@ -528,90 +617,32 @@ func (data *Data) collectDiagnostics(spn span.Span, msgSource, msg string) {
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) {
return func(src span.Span, expected []token.Pos) {
data.Completions[src] = Completion{
result := func(m map[span.Span]Completion, src span.Span, expected []token.Pos) {
m[src] = Completion{
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)
}
}
}