| // 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 ( |
| "context" |
| "go/token" |
| "path/filepath" |
| "reflect" |
| "sort" |
| "strings" |
| "testing" |
| |
| "golang.org/x/tools/go/packages" |
| "golang.org/x/tools/go/packages/packagestest" |
| "golang.org/x/tools/internal/lsp/protocol" |
| "golang.org/x/tools/internal/lsp/source" |
| ) |
| |
| func TestLSP(t *testing.T) { |
| packagestest.TestAll(t, testLSP) |
| } |
| |
| func testLSP(t *testing.T, exporter packagestest.Exporter) { |
| const dir = "testdata" |
| |
| files := packagestest.MustCopyFileTree(dir) |
| for fragment, operation := range files { |
| if trimmed := strings.TrimSuffix(fragment, ".in"); trimmed != fragment { |
| delete(files, fragment) |
| files[trimmed] = operation |
| } |
| } |
| modules := []packagestest.Module{ |
| { |
| Name: "golang.org/x/tools/internal/lsp", |
| Files: files, |
| }, |
| } |
| exported := packagestest.Export(t, exporter, modules) |
| defer exported.Cleanup() |
| |
| dirs := make(map[string]bool) |
| |
| // collect results for certain tests |
| expectedDiagnostics := make(map[string][]protocol.Diagnostic) |
| expectedCompletions := make(map[token.Position]*protocol.CompletionItem) |
| |
| s := &server{ |
| view: source.NewView(), |
| } |
| // merge the config objects |
| cfg := *exported.Config |
| cfg.Fset = s.view.Config.Fset |
| cfg.Mode = packages.LoadSyntax |
| s.view.Config = &cfg |
| |
| for _, module := range modules { |
| for fragment := range module.Files { |
| if !strings.HasSuffix(fragment, ".go") { |
| continue |
| } |
| filename := exporter.Filename(exported, module.Name, fragment) |
| expectedDiagnostics[filename] = []protocol.Diagnostic{} |
| dirs[filepath.Dir(filename)] = true |
| } |
| } |
| // Do a first pass to collect special markers |
| if err := exported.Expect(map[string]interface{}{ |
| "item": func(name string, r packagestest.Range, _, _ string) { |
| exported.Mark(name, r) |
| }, |
| }); err != nil { |
| t.Fatal(err) |
| } |
| // Collect any data that needs to be used by subsequent tests. |
| if err := exported.Expect(map[string]interface{}{ |
| "diag": func(pos token.Position, msg string) { |
| line := float64(pos.Line - 1) |
| col := float64(pos.Column - 1) |
| want := protocol.Diagnostic{ |
| Range: protocol.Range{ |
| Start: protocol.Position{ |
| Line: line, |
| Character: col, |
| }, |
| End: protocol.Position{ |
| Line: line, |
| Character: col, |
| }, |
| }, |
| Severity: protocol.SeverityError, |
| Source: "LSP", |
| Message: msg, |
| } |
| if _, ok := expectedDiagnostics[pos.Filename]; ok { |
| expectedDiagnostics[pos.Filename] = append(expectedDiagnostics[pos.Filename], want) |
| } else { |
| t.Errorf("unexpected filename: %v", pos.Filename) |
| } |
| }, |
| "item": func(pos token.Position, label, detail, kind string) { |
| var k protocol.CompletionItemKind |
| switch kind { |
| case "struct": |
| k = protocol.StructCompletion |
| case "func": |
| k = protocol.FunctionCompletion |
| case "var": |
| k = protocol.VariableCompletion |
| case "type": |
| k = protocol.TypeParameterCompletion |
| case "field": |
| k = protocol.FieldCompletion |
| case "interface": |
| k = protocol.InterfaceCompletion |
| case "const": |
| k = protocol.ConstantCompletion |
| case "method": |
| k = protocol.MethodCompletion |
| } |
| expectedCompletions[pos] = &protocol.CompletionItem{ |
| Label: label, |
| Detail: detail, |
| Kind: float64(k), |
| } |
| }, |
| }); err != nil { |
| t.Fatal(err) |
| } |
| |
| // test completion |
| testCompletion(t, exported, s, expectedCompletions) |
| |
| // test diagnostics |
| var dirList []string |
| for dir := range dirs { |
| dirList = append(dirList, dir) |
| } |
| exported.Config.Mode = packages.LoadFiles |
| pkgs, err := packages.Load(exported.Config, dirList...) |
| if err != nil { |
| t.Fatal(err) |
| } |
| testDiagnostics(t, s.view, pkgs, expectedDiagnostics) |
| } |
| |
| func testDiagnostics(t *testing.T, v *source.View, pkgs []*packages.Package, wants map[string][]protocol.Diagnostic) { |
| for _, pkg := range pkgs { |
| for _, filename := range pkg.GoFiles { |
| f := v.GetFile(source.ToURI(filename)) |
| diagnostics, err := source.Diagnostics(context.Background(), v, f) |
| if err != nil { |
| t.Fatal(err) |
| } |
| got := toProtocolDiagnostics(v, diagnostics[filename]) |
| sort.Slice(got, func(i int, j int) bool { |
| return got[i].Range.Start.Line < got[j].Range.Start.Line |
| }) |
| want := wants[filename] |
| if equal := reflect.DeepEqual(want, got); !equal { |
| t.Errorf("diagnostics failed for %s: (expected: %v), (got: %v)", filepath.Base(filename), want, got) |
| } |
| } |
| } |
| } |
| |
| func testCompletion(t *testing.T, exported *packagestest.Exported, s *server, wants map[token.Position]*protocol.CompletionItem) { |
| if err := exported.Expect(map[string]interface{}{ |
| "complete": func(src token.Position, expected []token.Position) { |
| var want []protocol.CompletionItem |
| for _, pos := range expected { |
| want = append(want, *wants[pos]) |
| } |
| list, err := s.Completion(context.Background(), &protocol.CompletionParams{ |
| TextDocumentPositionParams: protocol.TextDocumentPositionParams{ |
| TextDocument: protocol.TextDocumentIdentifier{ |
| URI: protocol.DocumentURI(source.ToURI(src.Filename)), |
| }, |
| Position: protocol.Position{ |
| Line: float64(src.Line - 1), |
| Character: float64(src.Column - 1), |
| }, |
| }, |
| }) |
| if err != nil { |
| t.Fatal(err) |
| } |
| got := list.Items |
| if equal := reflect.DeepEqual(want, got); !equal { |
| t.Errorf("completion failed for %s:%v:%v: (expected: %v), (got: %v)", filepath.Base(src.Filename), src.Line, src.Column, want, got) |
| } |
| }, |
| }); err != nil { |
| t.Fatal(err) |
| } |
| } |