| // Copyright 2019 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 tests |
| |
| import ( |
| "context" |
| "flag" |
| "go/ast" |
| "go/token" |
| "io/ioutil" |
| "path/filepath" |
| "sort" |
| "strings" |
| "testing" |
| |
| "golang.org/x/tools/go/packages" |
| "golang.org/x/tools/go/packages/packagestest" |
| "golang.org/x/tools/internal/lsp/source" |
| "golang.org/x/tools/internal/span" |
| "golang.org/x/tools/internal/txtar" |
| ) |
| |
| // 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 = 144 |
| ExpectedCompletionSnippetCount = 15 |
| ExpectedDiagnosticsCount = 17 |
| ExpectedFormatCount = 5 |
| ExpectedImportCount = 2 |
| ExpectedDefinitionsCount = 38 |
| ExpectedTypeDefinitionsCount = 2 |
| ExpectedHighlightsCount = 2 |
| ExpectedReferencesCount = 5 |
| ExpectedRenamesCount = 16 |
| ExpectedSymbolsCount = 1 |
| ExpectedSignaturesCount = 21 |
| ExpectedLinksCount = 2 |
| ) |
| |
| const ( |
| overlayFileSuffix = ".overlay" |
| goldenFileSuffix = ".golden" |
| inFileSuffix = ".in" |
| testModule = "golang.org/x/tools/internal/lsp" |
| ) |
| |
| 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 CompletionSnippets map[span.Span]CompletionSnippet |
| type Formats []span.Span |
| type Imports []span.Span |
| type Definitions map[span.Span]Definition |
| type Highlights map[string][]span.Span |
| type References map[span.Span][]span.Span |
| type Renames map[span.Span]string |
| type Symbols map[span.URI][]source.Symbol |
| type SymbolsChildren map[string][]source.Symbol |
| type Signatures map[span.Span]*source.SignatureInformation |
| type Links map[span.URI][]Link |
| |
| type Data struct { |
| Config packages.Config |
| Exported *packagestest.Exported |
| Diagnostics Diagnostics |
| CompletionItems CompletionItems |
| Completions Completions |
| CompletionSnippets CompletionSnippets |
| Formats Formats |
| Imports Imports |
| Definitions Definitions |
| Highlights Highlights |
| References References |
| Renames Renames |
| Symbols Symbols |
| symbolsChildren SymbolsChildren |
| Signatures Signatures |
| Links Links |
| |
| t testing.TB |
| fragments map[string]string |
| dir string |
| golden map[string]*Golden |
| } |
| |
| type Tests interface { |
| Diagnostics(*testing.T, Diagnostics) |
| Completion(*testing.T, Completions, CompletionSnippets, CompletionItems) |
| Format(*testing.T, Formats) |
| Import(*testing.T, Imports) |
| Definition(*testing.T, Definitions) |
| Highlight(*testing.T, Highlights) |
| Reference(*testing.T, References) |
| Rename(*testing.T, Renames) |
| Symbol(*testing.T, Symbols) |
| SignatureHelp(*testing.T, Signatures) |
| Link(*testing.T, Links) |
| } |
| |
| type Definition struct { |
| Name string |
| Src span.Span |
| IsType bool |
| OnlyHover bool |
| Def span.Span |
| } |
| |
| type CompletionSnippet struct { |
| CompletionItem token.Pos |
| PlainSnippet string |
| PlaceholderSnippet string |
| } |
| |
| type Link struct { |
| Src span.Span |
| Target string |
| } |
| |
| type Golden struct { |
| Filename string |
| Archive *txtar.Archive |
| Modified bool |
| } |
| |
| func Context(t testing.TB) context.Context { |
| return context.Background() |
| } |
| |
| func Load(t testing.TB, exporter packagestest.Exporter, dir string) *Data { |
| t.Helper() |
| |
| data := &Data{ |
| Diagnostics: make(Diagnostics), |
| CompletionItems: make(CompletionItems), |
| Completions: make(Completions), |
| CompletionSnippets: make(CompletionSnippets), |
| Definitions: make(Definitions), |
| Highlights: make(Highlights), |
| References: make(References), |
| Renames: make(Renames), |
| Symbols: make(Symbols), |
| symbolsChildren: make(SymbolsChildren), |
| Signatures: make(Signatures), |
| Links: make(Links), |
| |
| t: t, |
| dir: dir, |
| fragments: map[string]string{}, |
| golden: map[string]*Golden{}, |
| } |
| |
| files := packagestest.MustCopyFileTree(dir) |
| overlays := map[string][]byte{} |
| for fragment, operation := range files { |
| if trimmed := strings.TrimSuffix(fragment, goldenFileSuffix); trimmed != fragment { |
| delete(files, fragment) |
| goldFile := filepath.Join(dir, fragment) |
| archive, err := txtar.ParseFile(goldFile) |
| if err != nil { |
| t.Fatalf("could not read golden file %v: %v", fragment, err) |
| } |
| data.golden[trimmed] = &Golden{ |
| Filename: goldFile, |
| Archive: archive, |
| } |
| } else if trimmed := strings.TrimSuffix(fragment, inFileSuffix); trimmed != fragment { |
| delete(files, fragment) |
| files[trimmed] = operation |
| } else if index := strings.Index(fragment, overlayFileSuffix); index >= 0 { |
| delete(files, fragment) |
| partial := fragment[:index] + fragment[index+len(overlayFileSuffix):] |
| contents, err := ioutil.ReadFile(filepath.Join(dir, fragment)) |
| if err != nil { |
| t.Fatal(err) |
| } |
| overlays[partial] = contents |
| } |
| } |
| modules := []packagestest.Module{ |
| { |
| Name: testModule, |
| Files: files, |
| Overlay: overlays, |
| }, |
| } |
| data.Exported = packagestest.Export(t, exporter, modules) |
| for fragment, _ := range files { |
| filename := data.Exported.File(testModule, fragment) |
| data.fragments[filename] = fragment |
| } |
| |
| // Merge the exported.Config with the view.Config. |
| data.Config = *data.Exported.Config |
| data.Config.Fset = token.NewFileSet() |
| data.Config.Context = Context(nil) |
| data.Config.ParseFile = func(fset *token.FileSet, filename string, src []byte) (*ast.File, error) { |
| panic("ParseFile should not be called") |
| } |
| |
| // Do a first pass to collect special markers for completion. |
| if err := data.Exported.Expect(map[string]interface{}{ |
| "item": func(name string, r packagestest.Range, _, _ string) { |
| data.Exported.Mark(name, r) |
| }, |
| }); err != nil { |
| t.Fatal(err) |
| } |
| |
| // 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, |
| "format": data.collectFormats, |
| "import": data.collectImports, |
| "godef": data.collectDefinitions, |
| "typdef": data.collectTypeDefinitions, |
| "hover": data.collectHoverDefinitions, |
| "highlight": data.collectHighlights, |
| "refs": data.collectReferences, |
| "rename": data.collectRenames, |
| "symbol": data.collectSymbols, |
| "signature": data.collectSignatures, |
| "snippet": data.collectCompletionSnippets, |
| "link": data.collectLinks, |
| }); err != nil { |
| t.Fatal(err) |
| } |
| for _, symbols := range data.Symbols { |
| for i := range symbols { |
| children := data.symbolsChildren[symbols[i].Name] |
| symbols[i].Children = children |
| } |
| } |
| // Collect names for the entries that require golden files. |
| if err := data.Exported.Expect(map[string]interface{}{ |
| "godef": data.collectDefinitionNames, |
| "hover": data.collectDefinitionNames, |
| }); err != nil { |
| t.Fatal(err) |
| } |
| return 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) |
| } |
| 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) |
| }) |
| |
| t.Run("Diagnostics", func(t *testing.T) { |
| t.Helper() |
| diagnosticsCount := 0 |
| for _, want := range data.Diagnostics { |
| diagnosticsCount += len(want) |
| } |
| if diagnosticsCount != ExpectedDiagnosticsCount { |
| t.Errorf("got %v diagnostics expected %v", diagnosticsCount, ExpectedDiagnosticsCount) |
| } |
| tests.Diagnostics(t, data.Diagnostics) |
| }) |
| |
| t.Run("Format", func(t *testing.T) { |
| t.Helper() |
| if len(data.Formats) != ExpectedFormatCount { |
| t.Errorf("got %v formats expected %v", len(data.Formats), ExpectedFormatCount) |
| } |
| tests.Format(t, data.Formats) |
| }) |
| |
| t.Run("Import", func(t *testing.T) { |
| t.Helper() |
| if len(data.Imports) != ExpectedImportCount { |
| t.Errorf("got %v imports expected %v", len(data.Imports), ExpectedImportCount) |
| } |
| tests.Import(t, data.Imports) |
| }) |
| |
| t.Run("Definition", func(t *testing.T) { |
| t.Helper() |
| if len(data.Definitions) != ExpectedDefinitionsCount { |
| t.Errorf("got %v definitions expected %v", len(data.Definitions), ExpectedDefinitionsCount) |
| } |
| tests.Definition(t, data.Definitions) |
| }) |
| |
| t.Run("Highlight", func(t *testing.T) { |
| t.Helper() |
| if len(data.Highlights) != ExpectedHighlightsCount { |
| t.Errorf("got %v highlights expected %v", len(data.Highlights), ExpectedHighlightsCount) |
| } |
| tests.Highlight(t, data.Highlights) |
| }) |
| |
| t.Run("References", func(t *testing.T) { |
| t.Helper() |
| if len(data.References) != ExpectedReferencesCount { |
| t.Errorf("got %v references expected %v", len(data.References), ExpectedReferencesCount) |
| } |
| tests.Reference(t, data.References) |
| }) |
| |
| t.Run("Renames", func(t *testing.T) { |
| t.Helper() |
| if len(data.Renames) != ExpectedRenamesCount { |
| t.Errorf("got %v renames expected %v", len(data.Renames), ExpectedRenamesCount) |
| } |
| tests.Rename(t, data.Renames) |
| }) |
| |
| t.Run("Symbols", func(t *testing.T) { |
| t.Helper() |
| if len(data.Symbols) != ExpectedSymbolsCount { |
| t.Errorf("got %v symbols expected %v", len(data.Symbols), ExpectedSymbolsCount) |
| } |
| tests.Symbol(t, data.Symbols) |
| }) |
| |
| t.Run("SignatureHelp", func(t *testing.T) { |
| t.Helper() |
| if len(data.Signatures) != ExpectedSignaturesCount { |
| t.Errorf("got %v signatures expected %v", len(data.Signatures), ExpectedSignaturesCount) |
| } |
| tests.SignatureHelp(t, data.Signatures) |
| }) |
| |
| t.Run("Link", func(t *testing.T) { |
| t.Helper() |
| linksCount := 0 |
| for _, want := range data.Links { |
| linksCount += len(want) |
| } |
| if linksCount != ExpectedLinksCount { |
| t.Errorf("got %v links expected %v", linksCount, ExpectedLinksCount) |
| } |
| tests.Link(t, data.Links) |
| }) |
| |
| if *updateGolden { |
| for _, golden := range data.golden { |
| if !golden.Modified { |
| continue |
| } |
| sort.Slice(golden.Archive.Files, func(i, j int) bool { |
| return golden.Archive.Files[i].Name < golden.Archive.Files[j].Name |
| }) |
| if err := ioutil.WriteFile(golden.Filename, txtar.Format(golden.Archive), 0666); err != nil { |
| t.Fatal(err) |
| } |
| } |
| } |
| } |
| |
| func (data *Data) Golden(tag string, target string, update func() ([]byte, error)) []byte { |
| data.t.Helper() |
| fragment, found := data.fragments[target] |
| if !found { |
| if filepath.IsAbs(target) { |
| data.t.Fatalf("invalid golden file fragment %v", target) |
| } |
| fragment = target |
| } |
| golden := data.golden[fragment] |
| if golden == nil { |
| if !*updateGolden { |
| data.t.Fatalf("could not find golden file %v: %v", fragment, tag) |
| } |
| golden = &Golden{ |
| Filename: filepath.Join(data.dir, fragment+goldenFileSuffix), |
| Archive: &txtar.Archive{}, |
| Modified: true, |
| } |
| data.golden[fragment] = golden |
| } |
| var file *txtar.File |
| for i := range golden.Archive.Files { |
| f := &golden.Archive.Files[i] |
| if f.Name == tag { |
| file = f |
| break |
| } |
| } |
| if *updateGolden { |
| if file == nil { |
| golden.Archive.Files = append(golden.Archive.Files, txtar.File{ |
| Name: tag, |
| }) |
| file = &golden.Archive.Files[len(golden.Archive.Files)-1] |
| } |
| contents, err := update() |
| if err != nil { |
| data.t.Fatalf("could not update golden file %v: %v", fragment, err) |
| } |
| file.Data = append(contents, '\n') // add trailing \n for txtar |
| golden.Modified = true |
| } |
| if file == nil { |
| data.t.Fatalf("could not find golden contents %v: %v", fragment, tag) |
| } |
| return file.Data[:len(file.Data)-1] // drop the trailing \n |
| } |
| |
| func (data *Data) collectDiagnostics(spn span.Span, msgSource, msg string) { |
| if _, ok := data.Diagnostics[spn.URI()]; !ok { |
| data.Diagnostics[spn.URI()] = []source.Diagnostic{} |
| } |
| // If a file has an empty diagnostic message, return. This allows us to |
| // avoid testing diagnostics in files that may have a lot of them. |
| if msg == "" { |
| return |
| } |
| severity := source.SeverityError |
| if strings.Contains(string(spn.URI()), "analyzer") { |
| severity = source.SeverityWarning |
| } |
| want := source.Diagnostic{ |
| Span: spn, |
| Severity: severity, |
| Source: msgSource, |
| Message: msg, |
| } |
| data.Diagnostics[spn.URI()] = append(data.Diagnostics[spn.URI()], want) |
| } |
| |
| func (data *Data) collectCompletions(src span.Span, expected []token.Pos) { |
| data.Completions[src] = expected |
| } |
| |
| func (data *Data) collectCompletionItems(pos token.Pos, label, detail, kind string) { |
| data.CompletionItems[pos] = &source.CompletionItem{ |
| Label: label, |
| Detail: detail, |
| Kind: source.ParseCompletionItemKind(kind), |
| } |
| } |
| |
| func (data *Data) collectFormats(spn span.Span) { |
| data.Formats = append(data.Formats, spn) |
| } |
| |
| func (data *Data) collectImports(spn span.Span) { |
| data.Imports = append(data.Imports, spn) |
| } |
| |
| func (data *Data) collectDefinitions(src, target span.Span) { |
| data.Definitions[src] = Definition{ |
| Src: src, |
| Def: target, |
| } |
| } |
| |
| func (data *Data) collectHoverDefinitions(src, target span.Span) { |
| data.Definitions[src] = Definition{ |
| Src: src, |
| Def: target, |
| OnlyHover: true, |
| } |
| } |
| |
| func (data *Data) collectTypeDefinitions(src, target span.Span) { |
| data.Definitions[src] = Definition{ |
| Src: src, |
| Def: target, |
| IsType: true, |
| } |
| } |
| |
| func (data *Data) collectDefinitionNames(src span.Span, name string) { |
| d := data.Definitions[src] |
| d.Name = name |
| data.Definitions[src] = d |
| } |
| |
| func (data *Data) collectHighlights(name string, rng span.Span) { |
| data.Highlights[name] = append(data.Highlights[name], rng) |
| } |
| |
| func (data *Data) collectReferences(src span.Span, expected []span.Span) { |
| data.References[src] = expected |
| } |
| |
| func (data *Data) collectRenames(src span.Span, newText string) { |
| data.Renames[src] = newText |
| } |
| |
| func (data *Data) collectSymbols(name string, spn span.Span, kind string, parentName string) { |
| sym := source.Symbol{ |
| Name: name, |
| Kind: source.ParseSymbolKind(kind), |
| SelectionSpan: spn, |
| } |
| if parentName == "" { |
| data.Symbols[spn.URI()] = append(data.Symbols[spn.URI()], sym) |
| } else { |
| data.symbolsChildren[parentName] = append(data.symbolsChildren[parentName], sym) |
| } |
| } |
| |
| func (data *Data) collectSignatures(spn span.Span, signature string, activeParam int64) { |
| data.Signatures[spn] = &source.SignatureInformation{ |
| Label: signature, |
| ActiveParameter: int(activeParam), |
| } |
| // Hardcode special case to test the lack of a signature. |
| if signature == "" && activeParam == 0 { |
| data.Signatures[spn] = nil |
| } |
| } |
| |
| func (data *Data) collectCompletionSnippets(spn span.Span, item token.Pos, plain, placeholder string) { |
| data.CompletionSnippets[spn] = CompletionSnippet{ |
| CompletionItem: item, |
| PlainSnippet: plain, |
| PlaceholderSnippet: placeholder, |
| } |
| } |
| |
| func (data *Data) collectLinks(spn span.Span, link string) { |
| uri := spn.URI() |
| data.Links[uri] = append(data.Links[uri], Link{ |
| Src: spn, |
| Target: link, |
| }) |
| } |