|  | // Copyright 2018 The Go Authors. All rights reserved. | 
|  | // Use of this source code is governed by a BSD-style | 
|  | // license that can be found in the LICENSE file. | 
|  |  | 
|  | package lsp | 
|  |  | 
|  | import ( | 
|  | "bytes" | 
|  | "context" | 
|  | "fmt" | 
|  | "go/token" | 
|  | "os" | 
|  | "os/exec" | 
|  | "path/filepath" | 
|  | "sort" | 
|  | "strings" | 
|  | "testing" | 
|  |  | 
|  | "golang.org/x/tools/go/packages/packagestest" | 
|  | "golang.org/x/tools/internal/lsp/cache" | 
|  | "golang.org/x/tools/internal/lsp/diff" | 
|  | "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" | 
|  | "golang.org/x/tools/internal/testenv" | 
|  | ) | 
|  |  | 
|  | func TestMain(m *testing.M) { | 
|  | testenv.ExitIfSmallMachine() | 
|  | os.Exit(m.Run()) | 
|  | } | 
|  |  | 
|  | func TestLSP(t *testing.T) { | 
|  | packagestest.TestAll(t, testLSP) | 
|  | } | 
|  |  | 
|  | type runner struct { | 
|  | server *Server | 
|  | data   *tests.Data | 
|  | ctx    context.Context | 
|  | } | 
|  |  | 
|  | const viewName = "lsp_test" | 
|  |  | 
|  | func testLSP(t *testing.T, exporter packagestest.Exporter) { | 
|  | ctx := tests.Context(t) | 
|  | data := tests.Load(t, exporter, "testdata") | 
|  | defer data.Exported.Cleanup() | 
|  |  | 
|  | 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 | 
|  | session.SetOptions(options) | 
|  | options.Env = data.Config.Env | 
|  | session.NewView(ctx, viewName, span.FileURI(data.Config.Dir), options) | 
|  | for filename, content := range data.Config.Overlay { | 
|  | session.SetOverlay(span.FileURI(filename), content) | 
|  | } | 
|  |  | 
|  | r := &runner{ | 
|  | server: &Server{ | 
|  | session:     session, | 
|  | undelivered: make(map[span.URI][]source.Diagnostic), | 
|  | }, | 
|  | data: data, | 
|  | ctx:  ctx, | 
|  | } | 
|  |  | 
|  | tests.Run(t, r, data) | 
|  | } | 
|  |  | 
|  | // TODO: Actually test the LSP diagnostics function in this test. | 
|  | func (r *runner) Diagnostics(t *testing.T, data tests.Diagnostics) { | 
|  | v := r.server.session.View(viewName) | 
|  | for uri, want := range data { | 
|  | f, err := v.GetFile(r.ctx, uri) | 
|  | if err != nil { | 
|  | t.Fatalf("no file for %s: %v", f, err) | 
|  | } | 
|  | gof, ok := f.(source.GoFile) | 
|  | if !ok { | 
|  | t.Fatalf("%s is not a Go file: %v", uri, err) | 
|  | } | 
|  | results, err := source.Diagnostics(r.ctx, v, gof, nil) | 
|  | if err != nil { | 
|  | t.Fatal(err) | 
|  | } | 
|  | got := results[uri] | 
|  | // A special case to test that there are no diagnostics for a file. | 
|  | if len(want) == 1 && want[0].Source == "no_diagnostics" { | 
|  | if len(got) != 0 { | 
|  | t.Errorf("expected no diagnostics for %s, got %v", uri, got) | 
|  | } | 
|  | continue | 
|  | } | 
|  | if diff := tests.DiffDiagnostics(uri, want, got); diff != "" { | 
|  | t.Error(diff) | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | 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 | 
|  | } | 
|  | } | 
|  | if got == nil { | 
|  | t.Fatalf("%s: couldn't find completion matching %q", src.URI(), wantItem.Label) | 
|  | } | 
|  | var expected string | 
|  | if usePlaceholders { | 
|  | expected = want.PlaceholderSnippet | 
|  | } else { | 
|  | expected = want.PlainSnippet | 
|  | } | 
|  | 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() | 
|  | view := r.server.session.ViewOf(uri) | 
|  | original := view.Options() | 
|  | modified := original | 
|  |  | 
|  | // Test all folding ranges. | 
|  | modified.LineFoldingOnly = false | 
|  | view.SetOptions(modified) | 
|  | ranges, err := r.server.FoldingRange(r.ctx, &protocol.FoldingRangeParams{ | 
|  | TextDocument: protocol.TextDocumentIdentifier{ | 
|  | URI: protocol.NewURI(uri), | 
|  | }, | 
|  | }) | 
|  | if err != nil { | 
|  | t.Error(err) | 
|  | continue | 
|  | } | 
|  | r.foldingRanges(t, "foldingRange", uri, ranges) | 
|  |  | 
|  | // Test folding ranges with lineFoldingOnly = true. | 
|  | modified.LineFoldingOnly = true | 
|  | view.SetOptions(modified) | 
|  | ranges, err = r.server.FoldingRange(r.ctx, &protocol.FoldingRangeParams{ | 
|  | TextDocument: protocol.TextDocumentIdentifier{ | 
|  | URI: protocol.NewURI(uri), | 
|  | }, | 
|  | }) | 
|  | if err != nil { | 
|  | t.Error(err) | 
|  | continue | 
|  | } | 
|  | r.foldingRanges(t, "foldingRange-lineFolding", uri, ranges) | 
|  | view.SetOptions(original) | 
|  | } | 
|  | } | 
|  |  | 
|  | func (r *runner) foldingRanges(t *testing.T, prefix string, uri span.URI, ranges []protocol.FoldingRange) { | 
|  | m, err := r.mapper(uri) | 
|  | if err != nil { | 
|  | t.Fatal(err) | 
|  | } | 
|  | // Fold all ranges. | 
|  | nonOverlapping := nonOverlappingRanges(ranges) | 
|  | for i, rngs := range nonOverlapping { | 
|  | got, err := foldRanges(m, string(m.Content), rngs) | 
|  | if err != nil { | 
|  | t.Error(err) | 
|  | continue | 
|  | } | 
|  | tag := fmt.Sprintf("%s-%d", prefix, i) | 
|  | want := string(r.data.Golden(tag, uri.Filename(), func() ([]byte, error) { | 
|  | return []byte(got), nil | 
|  | })) | 
|  |  | 
|  | if want != got { | 
|  | t.Errorf("%s: foldingRanges failed for %s, expected:\n%v\ngot:\n%v", tag, uri.Filename(), want, got) | 
|  | } | 
|  | } | 
|  |  | 
|  | // Filter by kind. | 
|  | kinds := []protocol.FoldingRangeKind{protocol.Imports, protocol.Comment} | 
|  | for _, kind := range kinds { | 
|  | var kindOnly []protocol.FoldingRange | 
|  | for _, fRng := range ranges { | 
|  | if fRng.Kind == string(kind) { | 
|  | kindOnly = append(kindOnly, fRng) | 
|  | } | 
|  | } | 
|  |  | 
|  | nonOverlapping := nonOverlappingRanges(kindOnly) | 
|  | for i, rngs := range nonOverlapping { | 
|  | got, err := foldRanges(m, string(m.Content), rngs) | 
|  | if err != nil { | 
|  | t.Error(err) | 
|  | continue | 
|  | } | 
|  | tag := fmt.Sprintf("%s-%s-%d", prefix, kind, i) | 
|  | want := string(r.data.Golden(tag, uri.Filename(), func() ([]byte, error) { | 
|  | return []byte(got), nil | 
|  | })) | 
|  |  | 
|  | if want != got { | 
|  | t.Errorf("%s: foldingRanges failed for %s, expected:\n%v\ngot:\n%v", tag, uri.Filename(), want, got) | 
|  | } | 
|  | } | 
|  |  | 
|  | } | 
|  | } | 
|  |  | 
|  | func nonOverlappingRanges(ranges []protocol.FoldingRange) (res [][]protocol.FoldingRange) { | 
|  | for _, fRng := range ranges { | 
|  | setNum := len(res) | 
|  | for i := 0; i < len(res); i++ { | 
|  | canInsert := true | 
|  | for _, rng := range res[i] { | 
|  | if conflict(rng, fRng) { | 
|  | canInsert = false | 
|  | break | 
|  | } | 
|  | } | 
|  | if canInsert { | 
|  | setNum = i | 
|  | break | 
|  | } | 
|  | } | 
|  | if setNum == len(res) { | 
|  | res = append(res, []protocol.FoldingRange{}) | 
|  | } | 
|  | res[setNum] = append(res[setNum], fRng) | 
|  | } | 
|  | return res | 
|  | } | 
|  |  | 
|  | func conflict(a, b protocol.FoldingRange) bool { | 
|  | // a start position is <= b start positions | 
|  | return (a.StartLine < b.StartLine || (a.StartLine == b.StartLine && a.StartCharacter <= b.StartCharacter)) && | 
|  | (a.EndLine > b.StartLine || (a.EndLine == b.StartLine && a.EndCharacter > b.StartCharacter)) | 
|  | } | 
|  |  | 
|  | func foldRanges(m *protocol.ColumnMapper, contents string, ranges []protocol.FoldingRange) (string, error) { | 
|  | foldedText := "<>" | 
|  | res := contents | 
|  | // Apply the edits from the end of the file forward | 
|  | // to preserve the offsets | 
|  | for i := len(ranges) - 1; i >= 0; i-- { | 
|  | fRange := ranges[i] | 
|  | spn, err := m.RangeSpan(protocol.Range{ | 
|  | Start: protocol.Position{ | 
|  | Line:      fRange.StartLine, | 
|  | Character: fRange.StartCharacter, | 
|  | }, | 
|  | End: protocol.Position{ | 
|  | Line:      fRange.EndLine, | 
|  | Character: fRange.EndCharacter, | 
|  | }, | 
|  | }) | 
|  | if err != nil { | 
|  | return "", err | 
|  | } | 
|  | start := spn.Start().Offset() | 
|  | end := spn.End().Offset() | 
|  |  | 
|  | tmp := res[0:start] + foldedText | 
|  | res = tmp + res[end:] | 
|  | } | 
|  | return res, nil | 
|  | } | 
|  |  | 
|  | func (r *runner) Format(t *testing.T, data tests.Formats) { | 
|  | for _, spn := range data { | 
|  | uri := spn.URI() | 
|  | filename := uri.Filename() | 
|  | gofmted := string(r.data.Golden("gofmt", filename, func() ([]byte, error) { | 
|  | cmd := exec.Command("gofmt", filename) | 
|  | out, _ := cmd.Output() // ignore error, sometimes we have intentionally ungofmt-able files | 
|  | return out, nil | 
|  | })) | 
|  |  | 
|  | edits, err := r.server.Formatting(r.ctx, &protocol.DocumentFormattingParams{ | 
|  | TextDocument: protocol.TextDocumentIdentifier{ | 
|  | URI: protocol.NewURI(uri), | 
|  | }, | 
|  | }) | 
|  | if err != nil { | 
|  | if gofmted != "" { | 
|  | t.Error(err) | 
|  | } | 
|  | continue | 
|  | } | 
|  | m, err := r.mapper(uri) | 
|  | if err != nil { | 
|  | t.Fatal(err) | 
|  | } | 
|  | sedits, err := source.FromProtocolEdits(m, edits) | 
|  | if err != nil { | 
|  | t.Error(err) | 
|  | } | 
|  | got := diff.ApplyEdits(string(m.Content), sedits) | 
|  | if gofmted != got { | 
|  | t.Errorf("format failed for %s, expected:\n%v\ngot:\n%v", filename, gofmted, got) | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | func (r *runner) Import(t *testing.T, data tests.Imports) { | 
|  | for _, spn := range data { | 
|  | uri := spn.URI() | 
|  | filename := uri.Filename() | 
|  | goimported := string(r.data.Golden("goimports", filename, func() ([]byte, error) { | 
|  | cmd := exec.Command("goimports", filename) | 
|  | out, _ := cmd.Output() // ignore error, sometimes we have intentionally ungofmt-able files | 
|  | return out, nil | 
|  | })) | 
|  |  | 
|  | actions, err := r.server.CodeAction(r.ctx, &protocol.CodeActionParams{ | 
|  | TextDocument: protocol.TextDocumentIdentifier{ | 
|  | URI: protocol.NewURI(uri), | 
|  | }, | 
|  | }) | 
|  | if err != nil { | 
|  | if goimported != "" { | 
|  | t.Error(err) | 
|  | } | 
|  | continue | 
|  | } | 
|  | m, err := r.mapper(uri) | 
|  | if err != nil { | 
|  | t.Fatal(err) | 
|  | } | 
|  | var edits []protocol.TextEdit | 
|  | for _, a := range actions { | 
|  | if a.Title == "Organize Imports" { | 
|  | edits = (*a.Edit.Changes)[string(uri)] | 
|  | } | 
|  | } | 
|  | sedits, err := source.FromProtocolEdits(m, edits) | 
|  | if err != nil { | 
|  | t.Error(err) | 
|  | } | 
|  | got := diff.ApplyEdits(string(m.Content), sedits) | 
|  | if goimported != got { | 
|  | t.Errorf("import failed for %s, expected:\n%v\ngot:\n%v", filename, goimported, got) | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | func (r *runner) SuggestedFix(t *testing.T, data tests.SuggestedFixes) { | 
|  | for _, spn := range data { | 
|  | uri := spn.URI() | 
|  | filename := uri.Filename() | 
|  | v := r.server.session.ViewOf(uri) | 
|  | fixed := string(r.data.Golden("suggestedfix", filename, func() ([]byte, error) { | 
|  | cmd := exec.Command("suggestedfix", filename) // TODO(matloob): what do we do here? | 
|  | out, _ := cmd.Output()                        // ignore error, sometimes we have intentionally ungofmt-able files | 
|  | return out, nil | 
|  | })) | 
|  | f, err := getGoFile(r.ctx, v, uri) | 
|  | if err != nil { | 
|  | t.Fatal(err) | 
|  | } | 
|  | results, err := source.Diagnostics(r.ctx, v, f, nil) | 
|  | if err != nil { | 
|  | t.Fatal(err) | 
|  | } | 
|  | _ = results | 
|  | actions, err := r.server.CodeAction(r.ctx, &protocol.CodeActionParams{ | 
|  | TextDocument: protocol.TextDocumentIdentifier{ | 
|  | URI: protocol.NewURI(uri), | 
|  | }, | 
|  | Context: protocol.CodeActionContext{Only: []protocol.CodeActionKind{protocol.QuickFix}}, | 
|  | }) | 
|  | if err != nil { | 
|  | if fixed != "" { | 
|  | t.Error(err) | 
|  | } | 
|  | continue | 
|  | } | 
|  | m, err := r.mapper(f.URI()) | 
|  | if err != nil { | 
|  | t.Fatal(err) | 
|  | } | 
|  | var edits []protocol.TextEdit | 
|  | for _, a := range actions { | 
|  | if a.Title == "Remove" { | 
|  | edits = (*a.Edit.Changes)[string(uri)] | 
|  | } | 
|  | } | 
|  | sedits, err := source.FromProtocolEdits(m, edits) | 
|  | if err != nil { | 
|  | t.Error(err) | 
|  | } | 
|  | got := diff.ApplyEdits(string(m.Content), sedits) | 
|  | if fixed != got { | 
|  | t.Errorf("suggested fixes failed for %s, expected:\n%v\ngot:\n%v", filename, fixed, got) | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | func (r *runner) Definition(t *testing.T, data tests.Definitions) { | 
|  | for _, d := range data { | 
|  | sm, err := r.mapper(d.Src.URI()) | 
|  | if err != nil { | 
|  | t.Fatal(err) | 
|  | } | 
|  | loc, err := sm.Location(d.Src) | 
|  | if err != nil { | 
|  | t.Fatalf("failed for %v: %v", d.Src, err) | 
|  | } | 
|  | tdpp := protocol.TextDocumentPositionParams{ | 
|  | TextDocument: protocol.TextDocumentIdentifier{URI: loc.URI}, | 
|  | Position:     loc.Range.Start, | 
|  | } | 
|  | var locs []protocol.Location | 
|  | var hover *protocol.Hover | 
|  | if d.IsType { | 
|  | params := &protocol.TypeDefinitionParams{ | 
|  | tdpp, | 
|  | protocol.WorkDoneProgressParams{}, | 
|  | protocol.PartialResultParams{}, | 
|  | } | 
|  | locs, err = r.server.TypeDefinition(r.ctx, params) | 
|  | } else { | 
|  | params := &protocol.DefinitionParams{ | 
|  | tdpp, | 
|  | protocol.WorkDoneProgressParams{}, | 
|  | protocol.PartialResultParams{}, | 
|  | } | 
|  | locs, err = r.server.Definition(r.ctx, params) | 
|  | if err != nil { | 
|  | t.Fatalf("failed for %v: %+v", d.Src, err) | 
|  | } | 
|  | v := &protocol.HoverParams{ | 
|  | tdpp, | 
|  | protocol.WorkDoneProgressParams{}, | 
|  | } | 
|  | hover, err = r.server.Hover(r.ctx, v) | 
|  | } | 
|  | if err != nil { | 
|  | t.Fatalf("failed for %v: %v", d.Src, err) | 
|  | } | 
|  | if len(locs) != 1 { | 
|  | t.Errorf("got %d locations for definition, expected 1", len(locs)) | 
|  | } | 
|  | if hover != nil { | 
|  | tag := fmt.Sprintf("%s-hover", d.Name) | 
|  | expectHover := string(r.data.Golden(tag, d.Src.URI().Filename(), func() ([]byte, error) { | 
|  | return []byte(hover.Contents.Value), nil | 
|  | })) | 
|  | if hover.Contents.Value != expectHover { | 
|  | t.Errorf("for %v got %q want %q", d.Src, hover.Contents.Value, expectHover) | 
|  | } | 
|  | } else if !d.OnlyHover { | 
|  | locURI := span.NewURI(locs[0].URI) | 
|  | lm, err := r.mapper(locURI) | 
|  | if err != nil { | 
|  | t.Fatal(err) | 
|  | } | 
|  | if def, err := lm.Span(locs[0]); err != nil { | 
|  | t.Fatalf("failed for %v: %v", locs[0], err) | 
|  | } else if def != d.Def { | 
|  | t.Errorf("for %v got %v want %v", d.Src, def, d.Def) | 
|  | } | 
|  | } else { | 
|  | t.Errorf("no tests ran for %s", d.Src.URI()) | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | func (r *runner) Highlight(t *testing.T, data tests.Highlights) { | 
|  | for name, locations := range data { | 
|  | m, err := r.mapper(locations[0].URI()) | 
|  | if err != nil { | 
|  | t.Fatal(err) | 
|  | } | 
|  | loc, err := m.Location(locations[0]) | 
|  | if err != nil { | 
|  | t.Fatalf("failed for %v: %v", locations[0], err) | 
|  | } | 
|  | tdpp := protocol.TextDocumentPositionParams{ | 
|  | TextDocument: protocol.TextDocumentIdentifier{URI: loc.URI}, | 
|  | Position:     loc.Range.Start, | 
|  | } | 
|  | params := &protocol.DocumentHighlightParams{ | 
|  | tdpp, | 
|  | protocol.WorkDoneProgressParams{}, | 
|  | protocol.PartialResultParams{}, | 
|  | } | 
|  | highlights, err := r.server.DocumentHighlight(r.ctx, params) | 
|  | if err != nil { | 
|  | t.Fatal(err) | 
|  | } | 
|  | if len(highlights) != len(locations) { | 
|  | t.Fatalf("got %d highlights for %s, expected %d", len(highlights), name, len(locations)) | 
|  | } | 
|  | for i := range highlights { | 
|  | if h, err := m.RangeSpan(highlights[i].Range); err != nil { | 
|  | t.Fatalf("failed for %v: %v", highlights[i], err) | 
|  | } else if h != locations[i] { | 
|  | t.Errorf("want %v, got %v\n", locations[i], h) | 
|  | } | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | func (r *runner) Reference(t *testing.T, data tests.References) { | 
|  | for src, itemList := range data { | 
|  | sm, err := r.mapper(src.URI()) | 
|  | if err != nil { | 
|  | t.Fatal(err) | 
|  | } | 
|  | loc, err := sm.Location(src) | 
|  | if err != nil { | 
|  | t.Fatalf("failed for %v: %v", src, err) | 
|  | } | 
|  |  | 
|  | want := make(map[protocol.Location]bool) | 
|  | for _, pos := range itemList { | 
|  | m, err := r.mapper(pos.URI()) | 
|  | if err != nil { | 
|  | t.Fatal(err) | 
|  | } | 
|  | loc, err := m.Location(pos) | 
|  | if err != nil { | 
|  | t.Fatalf("failed for %v: %v", src, err) | 
|  | } | 
|  | want[loc] = true | 
|  | } | 
|  | params := &protocol.ReferenceParams{ | 
|  | TextDocumentPositionParams: protocol.TextDocumentPositionParams{ | 
|  | TextDocument: protocol.TextDocumentIdentifier{URI: loc.URI}, | 
|  | Position:     loc.Range.Start, | 
|  | }, | 
|  | } | 
|  | got, err := r.server.References(r.ctx, params) | 
|  | if err != nil { | 
|  | t.Fatalf("failed for %v: %v", src, err) | 
|  | } | 
|  | if len(got) != len(want) { | 
|  | t.Errorf("references failed: different lengths got %v want %v", len(got), len(want)) | 
|  | } | 
|  | for _, loc := range got { | 
|  | if !want[loc] { | 
|  | t.Errorf("references failed: incorrect references got %v want %v", loc, want) | 
|  | } | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | func (r *runner) Rename(t *testing.T, data tests.Renames) { | 
|  | for spn, newText := range data { | 
|  | tag := fmt.Sprintf("%s-rename", newText) | 
|  |  | 
|  | uri := spn.URI() | 
|  | filename := uri.Filename() | 
|  | sm, err := r.mapper(uri) | 
|  | if err != nil { | 
|  | t.Fatal(err) | 
|  | } | 
|  | loc, err := sm.Location(spn) | 
|  | if err != nil { | 
|  | t.Fatalf("failed for %v: %v", spn, err) | 
|  | } | 
|  |  | 
|  | workspaceEdits, err := r.server.Rename(r.ctx, &protocol.RenameParams{ | 
|  | TextDocument: protocol.TextDocumentIdentifier{ | 
|  | URI: protocol.NewURI(uri), | 
|  | }, | 
|  | Position: loc.Range.Start, | 
|  | NewName:  newText, | 
|  | }) | 
|  | if err != nil { | 
|  | renamed := string(r.data.Golden(tag, filename, func() ([]byte, error) { | 
|  | return []byte(err.Error()), nil | 
|  | })) | 
|  | if err.Error() != renamed { | 
|  | t.Errorf("rename failed for %s, expected:\n%v\ngot:\n%v\n", newText, renamed, err) | 
|  | } | 
|  | continue | 
|  | } | 
|  |  | 
|  | var res []string | 
|  | for uri, edits := range *workspaceEdits.Changes { | 
|  | m, err := r.mapper(span.URI(uri)) | 
|  | if err != nil { | 
|  | t.Fatal(err) | 
|  | } | 
|  | sedits, err := source.FromProtocolEdits(m, edits) | 
|  | if err != nil { | 
|  | t.Error(err) | 
|  | } | 
|  | filename := filepath.Base(m.URI.Filename()) | 
|  | contents := applyEdits(string(m.Content), sedits) | 
|  | res = append(res, fmt.Sprintf("%s:\n%s", filename, contents)) | 
|  | } | 
|  |  | 
|  | // Sort on filename | 
|  | sort.Strings(res) | 
|  |  | 
|  | var got string | 
|  | for i, val := range res { | 
|  | if i != 0 { | 
|  | got += "\n" | 
|  | } | 
|  | got += val | 
|  | } | 
|  |  | 
|  | renamed := string(r.data.Golden(tag, filename, func() ([]byte, error) { | 
|  | return []byte(got), nil | 
|  | })) | 
|  |  | 
|  | if renamed != got { | 
|  | t.Errorf("rename failed for %s, expected:\n%v\ngot:\n%v", newText, renamed, got) | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | func (r *runner) PrepareRename(t *testing.T, data tests.PrepareRenames) { | 
|  | for src, want := range data { | 
|  | m, err := r.mapper(src.URI()) | 
|  | if err != nil { | 
|  | t.Fatal(err) | 
|  | } | 
|  | loc, err := m.Location(src) | 
|  | if err != nil { | 
|  | t.Fatalf("failed for %v: %v", src, err) | 
|  | } | 
|  | tdpp := protocol.TextDocumentPositionParams{ | 
|  | TextDocument: protocol.TextDocumentIdentifier{URI: loc.URI}, | 
|  | Position:     loc.Range.Start, | 
|  | } | 
|  | params := &protocol.PrepareRenameParams{ | 
|  | tdpp, | 
|  | protocol.WorkDoneProgressParams{}, | 
|  | } | 
|  | got, err := r.server.PrepareRename(context.Background(), params) | 
|  | if err != nil { | 
|  | t.Errorf("prepare rename failed for %v: got error: %v", src, err) | 
|  | continue | 
|  | } | 
|  | if got == nil { | 
|  | if want.Text != "" { // expected an ident. | 
|  | t.Errorf("prepare rename failed for %v: got nil", src) | 
|  | } | 
|  | continue | 
|  | } | 
|  | if protocol.CompareRange(*got, want.Range) != 0 { | 
|  | t.Errorf("prepare rename failed: incorrect range got %v want %v", *got, want.Range) | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | func applyEdits(contents string, edits []diff.TextEdit) string { | 
|  | res := contents | 
|  |  | 
|  | // Apply the edits from the end of the file forward | 
|  | // to preserve the offsets | 
|  | for i := len(edits) - 1; i >= 0; i-- { | 
|  | edit := edits[i] | 
|  | start := edit.Span.Start().Offset() | 
|  | end := edit.Span.End().Offset() | 
|  | tmp := res[0:start] + edit.NewText | 
|  | res = tmp + res[end:] | 
|  | } | 
|  | return res | 
|  | } | 
|  |  | 
|  | func (r *runner) Symbol(t *testing.T, data tests.Symbols) { | 
|  | for uri, expectedSymbols := range data { | 
|  | params := &protocol.DocumentSymbolParams{ | 
|  | TextDocument: protocol.TextDocumentIdentifier{ | 
|  | URI: string(uri), | 
|  | }, | 
|  | } | 
|  | symbols, err := r.server.DocumentSymbol(r.ctx, params) | 
|  | if err != nil { | 
|  | t.Fatal(err) | 
|  | } | 
|  |  | 
|  | if len(symbols) != len(expectedSymbols) { | 
|  | t.Errorf("want %d top-level symbols in %v, got %d", len(expectedSymbols), uri, len(symbols)) | 
|  | continue | 
|  | } | 
|  | if diff := r.diffSymbols(t, uri, expectedSymbols, symbols); diff != "" { | 
|  | t.Error(diff) | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | func (r *runner) diffSymbols(t *testing.T, uri span.URI, want []protocol.DocumentSymbol, got []protocol.DocumentSymbol) string { | 
|  | sort.Slice(want, func(i, j int) bool { return want[i].Name < want[j].Name }) | 
|  | sort.Slice(got, func(i, j int) bool { return got[i].Name < got[j].Name }) | 
|  | if len(got) != len(want) { | 
|  | return summarizeSymbols(t, -1, want, got, "different lengths got %v want %v", len(got), len(want)) | 
|  | } | 
|  | for i, w := range want { | 
|  | g := got[i] | 
|  | if w.Name != g.Name { | 
|  | return summarizeSymbols(t, i, want, got, "incorrect name got %v want %v", g.Name, w.Name) | 
|  | } | 
|  | if w.Kind != g.Kind { | 
|  | return summarizeSymbols(t, i, want, got, "incorrect kind got %v want %v", g.Kind, w.Kind) | 
|  | } | 
|  | if protocol.CompareRange(g.SelectionRange, w.SelectionRange) != 0 { | 
|  | return summarizeSymbols(t, i, want, got, "incorrect span got %v want %v", g.SelectionRange, w.SelectionRange) | 
|  | } | 
|  | if msg := r.diffSymbols(t, uri, w.Children, g.Children); msg != "" { | 
|  | return fmt.Sprintf("children of %s: %s", w.Name, msg) | 
|  | } | 
|  | } | 
|  | return "" | 
|  | } | 
|  |  | 
|  | func summarizeSymbols(t *testing.T, i int, want []protocol.DocumentSymbol, got []protocol.DocumentSymbol, reason string, args ...interface{}) string { | 
|  | msg := &bytes.Buffer{} | 
|  | fmt.Fprint(msg, "document symbols 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 _, s := range want { | 
|  | fmt.Fprintf(msg, "  %v %v %v\n", s.Name, s.Kind, s.SelectionRange) | 
|  | } | 
|  | fmt.Fprintf(msg, "got:\n") | 
|  | for _, s := range got { | 
|  | fmt.Fprintf(msg, "  %v %v %v\n", s.Name, s.Kind, s.SelectionRange) | 
|  | } | 
|  | return msg.String() | 
|  | } | 
|  |  | 
|  | func (r *runner) SignatureHelp(t *testing.T, data tests.Signatures) { | 
|  | for spn, expectedSignatures := range data { | 
|  | m, err := r.mapper(spn.URI()) | 
|  | if err != nil { | 
|  | t.Fatal(err) | 
|  | } | 
|  | loc, err := m.Location(spn) | 
|  | if err != nil { | 
|  | t.Fatalf("failed for %v: %v", loc, err) | 
|  | } | 
|  | tdpp := protocol.TextDocumentPositionParams{ | 
|  | TextDocument: protocol.TextDocumentIdentifier{ | 
|  | URI: protocol.NewURI(spn.URI()), | 
|  | }, | 
|  | Position: loc.Range.Start, | 
|  | } | 
|  | params := &protocol.SignatureHelpParams{ | 
|  | tdpp, | 
|  | protocol.WorkDoneProgressParams{}, | 
|  | } | 
|  | gotSignatures, err := r.server.SignatureHelp(r.ctx, params) | 
|  | if err != nil { | 
|  | // Only fail if we got an error we did not expect. | 
|  | if expectedSignatures != nil { | 
|  | t.Fatal(err) | 
|  | } | 
|  | continue | 
|  | } | 
|  | if expectedSignatures == nil { | 
|  | if gotSignatures != nil { | 
|  | t.Errorf("expected no signature, got %v", gotSignatures) | 
|  | } | 
|  | continue | 
|  | } | 
|  | if diff := diffSignatures(spn, expectedSignatures, gotSignatures); diff != "" { | 
|  | t.Error(diff) | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | func diffSignatures(spn span.Span, want *source.SignatureInformation, got *protocol.SignatureHelp) string { | 
|  | decorate := func(f string, args ...interface{}) string { | 
|  | return fmt.Sprintf("Invalid signature at %s: %s", spn, fmt.Sprintf(f, args...)) | 
|  | } | 
|  |  | 
|  | if len(got.Signatures) != 1 { | 
|  | return decorate("wanted 1 signature, got %d", len(got.Signatures)) | 
|  | } | 
|  |  | 
|  | if got.ActiveSignature != 0 { | 
|  | return decorate("wanted active signature of 0, got %f", got.ActiveSignature) | 
|  | } | 
|  |  | 
|  | if want.ActiveParameter != int(got.ActiveParameter) { | 
|  | return decorate("wanted active parameter of %d, got %f", want.ActiveParameter, got.ActiveParameter) | 
|  | } | 
|  |  | 
|  | gotSig := got.Signatures[int(got.ActiveSignature)] | 
|  |  | 
|  | if want.Label != gotSig.Label { | 
|  | return decorate("wanted label %q, got %q", want.Label, gotSig.Label) | 
|  | } | 
|  |  | 
|  | var paramParts []string | 
|  | for _, p := range gotSig.Parameters { | 
|  | paramParts = append(paramParts, p.Label) | 
|  | } | 
|  | paramsStr := strings.Join(paramParts, ", ") | 
|  | if !strings.Contains(gotSig.Label, paramsStr) { | 
|  | return decorate("expected signature %q to contain params %q", gotSig.Label, paramsStr) | 
|  | } | 
|  |  | 
|  | return "" | 
|  | } | 
|  |  | 
|  | func (r *runner) Link(t *testing.T, data tests.Links) { | 
|  | for uri, wantLinks := range data { | 
|  | m, err := r.mapper(uri) | 
|  | if err != nil { | 
|  | t.Fatal(err) | 
|  | } | 
|  | gotLinks, err := r.server.DocumentLink(r.ctx, &protocol.DocumentLinkParams{ | 
|  | TextDocument: protocol.TextDocumentIdentifier{ | 
|  | URI: protocol.NewURI(uri), | 
|  | }, | 
|  | }) | 
|  | if err != nil { | 
|  | t.Fatal(err) | 
|  | } | 
|  | var notePositions []token.Position | 
|  | links := make(map[span.Span]string, len(wantLinks)) | 
|  | for _, link := range wantLinks { | 
|  | links[link.Src] = link.Target | 
|  | notePositions = append(notePositions, link.NotePosition) | 
|  | } | 
|  |  | 
|  | for _, link := range gotLinks { | 
|  | spn, err := m.RangeSpan(link.Range) | 
|  | if err != nil { | 
|  | t.Fatal(err) | 
|  | } | 
|  | linkInNote := false | 
|  | for _, notePosition := range notePositions { | 
|  | // Drop the links found inside expectation notes arguments as this links are not collected by expect package | 
|  | if notePosition.Line == spn.Start().Line() && | 
|  | notePosition.Column <= spn.Start().Column() { | 
|  | delete(links, spn) | 
|  | linkInNote = true | 
|  | } | 
|  | } | 
|  | if linkInNote { | 
|  | continue | 
|  | } | 
|  | if target, ok := links[spn]; ok { | 
|  | delete(links, spn) | 
|  | if target != link.Target { | 
|  | t.Errorf("for %v want %v, got %v\n", spn, link.Target, target) | 
|  | } | 
|  | } else { | 
|  | t.Errorf("unexpected link %v:%v\n", spn, link.Target) | 
|  | } | 
|  | } | 
|  | for spn, target := range links { | 
|  | t.Errorf("missing link %v:%v\n", spn, target) | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | func (r *runner) mapper(uri span.URI) (*protocol.ColumnMapper, error) { | 
|  | filename := uri.Filename() | 
|  | fset := r.data.Exported.ExpectFileSet | 
|  | var f *token.File | 
|  | fset.Iterate(func(check *token.File) bool { | 
|  | if check.Name() == filename { | 
|  | f = check | 
|  | return false | 
|  | } | 
|  | return true | 
|  | }) | 
|  | if f == nil { | 
|  | return nil, fmt.Errorf("no token.File for %s", uri) | 
|  | } | 
|  | content, err := r.data.Exported.FileContents(f.Name()) | 
|  | if err != nil { | 
|  | return nil, err | 
|  | } | 
|  | return protocol.NewColumnMapper(uri, filename, fset, f, content), nil | 
|  | } | 
|  |  | 
|  | func TestBytesOffset(t *testing.T) { | 
|  | tests := []struct { | 
|  | text string | 
|  | pos  protocol.Position | 
|  | want int | 
|  | }{ | 
|  | {text: `a𐐀b`, pos: protocol.Position{Line: 0, Character: 0}, want: 0}, | 
|  | {text: `a𐐀b`, pos: protocol.Position{Line: 0, Character: 1}, want: 1}, | 
|  | {text: `a𐐀b`, pos: protocol.Position{Line: 0, Character: 2}, want: 1}, | 
|  | {text: `a𐐀b`, pos: protocol.Position{Line: 0, Character: 3}, want: 5}, | 
|  | {text: `a𐐀b`, pos: protocol.Position{Line: 0, Character: 4}, want: 6}, | 
|  | {text: `a𐐀b`, pos: protocol.Position{Line: 0, Character: 5}, want: -1}, | 
|  | {text: "aaa\nbbb\n", pos: protocol.Position{Line: 0, Character: 3}, want: 3}, | 
|  | {text: "aaa\nbbb\n", pos: protocol.Position{Line: 0, Character: 4}, want: 3}, | 
|  | {text: "aaa\nbbb\n", pos: protocol.Position{Line: 1, Character: 0}, want: 4}, | 
|  | {text: "aaa\nbbb\n", pos: protocol.Position{Line: 1, Character: 3}, want: 7}, | 
|  | {text: "aaa\nbbb\n", pos: protocol.Position{Line: 1, Character: 4}, want: 7}, | 
|  | {text: "aaa\nbbb\n", pos: protocol.Position{Line: 2, Character: 0}, want: 8}, | 
|  | {text: "aaa\nbbb\n", pos: protocol.Position{Line: 2, Character: 1}, want: -1}, | 
|  | {text: "aaa\nbbb\n\n", pos: protocol.Position{Line: 2, Character: 0}, want: 8}, | 
|  | } | 
|  |  | 
|  | for i, test := range tests { | 
|  | fname := fmt.Sprintf("test %d", i) | 
|  | fset := token.NewFileSet() | 
|  | f := fset.AddFile(fname, -1, len(test.text)) | 
|  | f.SetLinesForContent([]byte(test.text)) | 
|  | mapper := protocol.NewColumnMapper(span.FileURI(fname), fname, fset, f, []byte(test.text)) | 
|  | got, err := mapper.Point(test.pos) | 
|  | if err != nil && test.want != -1 { | 
|  | t.Errorf("unexpected error: %v", err) | 
|  | } | 
|  | if err == nil && got.Offset() != test.want { | 
|  | t.Errorf("want %d for %q(Line:%d,Character:%d), but got %d", test.want, test.text, int(test.pos.Line), int(test.pos.Character), got.Offset()) | 
|  | } | 
|  | } | 
|  | } |