From 5797d2e2980b609df5113ff194cf83922d7b213e Mon Sep 17 00:00:00 2001 From: Muir Manders Date: Mon, 17 Jun 2019 20:56:06 -0700 Subject: [PATCH] internal/lsp: add more flexible completion tests Add a new @completePartial note that does not require you to specify the full list of completions. This gets rid of a lot of noise when you just want to test the relative order of some completion candidates but don't care about all the other candidates in scope. I changed one existing test to use @completePartial as an example. Change-Id: I56005405477e562803f094c0cac05ef2b854ad1a Reviewed-on: https://go-review.googlesource.com/c/tools/+/192657 Run-TryBot: Rebecca Stambler TryBot-Result: Gobot Gobot Reviewed-by: Rebecca Stambler --- internal/lsp/lsp_test.go | 55 ++++++++++++++-- internal/lsp/source/source_test.go | 54 ++++++++++++++-- .../testdata/deepcomplete/deep_complete.go | 11 ++-- internal/lsp/tests/tests.go | 63 ++++++++++++------- 4 files changed, 146 insertions(+), 37 deletions(-) diff --git a/internal/lsp/lsp_test.go b/internal/lsp/lsp_test.go index f1803329a0..fd9fbe7ec6 100644 --- a/internal/lsp/lsp_test.go +++ b/internal/lsp/lsp_test.go @@ -117,9 +117,9 @@ func (r *runner) Completion(t *testing.T, data tests.Completions, snippets tests // Set this as a default. modified.Completion.Documentation = true - for src, itemList := range data { + for src, test := range data { var want []source.CompletionItem - for _, pos := range itemList { + for _, pos := range test.CompletionItems { want = append(want, *items[pos]) } @@ -138,8 +138,16 @@ func (r *runner) Completion(t *testing.T, data tests.Completions, snippets tests } got = append(got, item) } - if diff := diffCompletionItems(t, src, want, got); diff != "" { - t.Errorf("%s: %s", src, diff) + + 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) + } } } @@ -219,7 +227,7 @@ func isBuiltin(item protocol.CompletionItem) bool { // diffCompletionItems prints the diff between expected and actual completion // test results. -func diffCompletionItems(t *testing.T, spn span.Span, want []source.CompletionItem, got []protocol.CompletionItem) string { +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)) } @@ -243,6 +251,43 @@ func diffCompletionItems(t *testing.T, spn span.Span, want []source.CompletionIt 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") diff --git a/internal/lsp/source/source_test.go b/internal/lsp/source/source_test.go index 6271706892..51b220e2aa 100644 --- a/internal/lsp/source/source_test.go +++ b/internal/lsp/source/source_test.go @@ -86,9 +86,9 @@ 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 - for src, itemList := range data { + for src, test := range data { var want []source.CompletionItem - for _, pos := range itemList { + for _, pos := range test.CompletionItems { want = append(want, *items[pos]) } f, err := r.view.GetFile(ctx, src.URI()) @@ -142,8 +142,15 @@ func (r *runner) Completion(t *testing.T, data tests.Completions, snippets tests } got = append(got, item) } - if diff := diffCompletionItems(t, src, want, got); diff != "" { - t.Errorf("%s: %s", src, diff) + 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) + } } } for _, usePlaceholders := range []bool{true, false} { @@ -207,7 +214,7 @@ func isBuiltin(item source.CompletionItem) bool { // diffCompletionItems prints the diff between expected and actual completion // test results. -func diffCompletionItems(t *testing.T, spn span.Span, want []source.CompletionItem, got []source.CompletionItem) string { +func diffCompletionItems(want []source.CompletionItem, got []source.CompletionItem) string { sort.SliceStable(got, func(i, j int) bool { return got[i].Score > got[j].Score }) @@ -250,6 +257,43 @@ func diffCompletionItems(t *testing.T, spn span.Span, want []source.CompletionIt 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") diff --git a/internal/lsp/testdata/deepcomplete/deep_complete.go b/internal/lsp/testdata/deepcomplete/deep_complete.go index 7ebed6959e..66d4859639 100644 --- a/internal/lsp/testdata/deepcomplete/deep_complete.go +++ b/internal/lsp/testdata/deepcomplete/deep_complete.go @@ -4,7 +4,7 @@ package deepcomplete -import "context" //@item(ctxPackage, "context", "\"context\"", "package") +import "context" type deepA struct { b deepB //@item(deepBField, "b", "deepB", "field") @@ -26,15 +26,14 @@ func _() { func wantsContext(context.Context) {} 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.") - context.WithValue(nil, nil, nil) //@item(ctxWithValue, "context.WithValue", "func(parent context.Context, key interface{}, val interface{}) context.Context", "func", "WithValue returns a copy of parent in which the value associated with key is val.") + 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) //@complete(")", ctxBackground, ctxTODO, ctxWithValue, ctxPackage) + wantsContext(c) //@completePartial(")", ctxBackground, ctxTODO) } func _() { - type deepCircle struct { //@item(deepCircleStruct, "deepCircle", "struct{...}", "struct") + type deepCircle struct { *deepCircle } var circle deepCircle //@item(deepCircle, "circle", "deepCircle", "var") diff --git a/internal/lsp/tests/tests.go b/internal/lsp/tests/tests.go index d81f42e94c..92a726036c 100644 --- a/internal/lsp/tests/tests.go +++ b/internal/lsp/tests/tests.go @@ -58,7 +58,7 @@ var updateGolden = flag.Bool("golden", false, "Update golden files") type Diagnostics map[span.URI][]source.Diagnostic type CompletionItems map[token.Pos]*source.CompletionItem -type Completions map[span.Span][]token.Pos +type Completions map[span.Span]Completion type CompletionSnippets map[span.Span]CompletionSnippet type FoldingRanges []span.Span type Formats []span.Span @@ -125,6 +125,21 @@ type Definition struct { Src, Def span.Span } +type CompletionTestType int + +const ( + // Full means candidates in test must match full list of candidates. + CompletionFull CompletionTestType = iota + + // Partial means candidates in test must be valid and in the right relative order. + CompletionPartial +) + +type Completion struct { + CompletionItems []token.Pos + Type CompletionTestType +} + type CompletionSnippet struct { CompletionItem token.Pos PlainSnippet string @@ -232,24 +247,25 @@ func Load(t testing.TB, exporter packagestest.Exporter, dir string) *Data { // Collect any data that needs to be used by subsequent tests. if err := data.Exported.Expect(map[string]interface{}{ - "diag": data.collectDiagnostics, - "item": data.collectCompletionItems, - "complete": data.collectCompletions, - "fold": data.collectFoldingRanges, - "format": data.collectFormats, - "import": data.collectImports, - "godef": data.collectDefinitions, - "typdef": data.collectTypeDefinitions, - "hover": data.collectHoverDefinitions, - "highlight": data.collectHighlights, - "refs": data.collectReferences, - "rename": data.collectRenames, - "prepare": data.collectPrepareRenames, - "symbol": data.collectSymbols, - "signature": data.collectSignatures, - "snippet": data.collectCompletionSnippets, - "link": data.collectLinks, - "suggestedfix": data.collectSuggestedFixes, + "diag": data.collectDiagnostics, + "item": data.collectCompletionItems, + "complete": data.collectCompletions(CompletionFull), + "completePartial": data.collectCompletions(CompletionPartial), + "fold": data.collectFoldingRanges, + "format": data.collectFormats, + "import": data.collectImports, + "godef": data.collectDefinitions, + "typdef": data.collectTypeDefinitions, + "hover": data.collectHoverDefinitions, + "highlight": data.collectHighlights, + "refs": data.collectReferences, + "rename": data.collectRenames, + "prepare": data.collectPrepareRenames, + "symbol": data.collectSymbols, + "signature": data.collectSignatures, + "snippet": data.collectCompletionSnippets, + "link": data.collectLinks, + "suggestedfix": data.collectSuggestedFixes, }); err != nil { t.Fatal(err) } @@ -567,8 +583,13 @@ func summarizeDiagnostics(i int, want []source.Diagnostic, got []source.Diagnost return msg.String() } -func (data *Data) collectCompletions(src span.Span, expected []token.Pos) { - data.Completions[src] = expected +func (data *Data) collectCompletions(typ CompletionTestType) func(span.Span, []token.Pos) { + return func(src span.Span, expected []token.Pos) { + data.Completions[src] = Completion{ + CompletionItems: expected, + Type: typ, + } + } } func (data *Data) collectCompletionItems(pos token.Pos, args []string) {