| // 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 exports functionality to be used across a variety of gopls tests. |
| package tests |
| |
| import ( |
| "bytes" |
| "context" |
| "flag" |
| "fmt" |
| "go/ast" |
| "go/token" |
| "io/ioutil" |
| "os" |
| "path/filepath" |
| "regexp" |
| "runtime" |
| "sort" |
| "strconv" |
| "strings" |
| "sync" |
| "testing" |
| "time" |
| |
| "golang.org/x/tools/go/expect" |
| "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" |
| "golang.org/x/tools/internal/span" |
| "golang.org/x/tools/txtar" |
| ) |
| |
| const ( |
| overlayFileSuffix = ".overlay" |
| goldenFileSuffix = ".golden" |
| inFileSuffix = ".in" |
| summaryFile = "summary.txt" |
| testModule = "golang.org/x/tools/internal/lsp" |
| ) |
| |
| var UpdateGolden = flag.Bool("golden", false, "Update golden files") |
| |
| type CodeLens map[span.URI][]protocol.CodeLens |
| 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 CaseSensitiveCompletions map[span.Span][]Completion |
| type RankCompletions map[span.Span][]Completion |
| type FoldingRanges []span.Span |
| type Formats []span.Span |
| type Imports []span.Span |
| type SuggestedFixes map[span.Span][]string |
| type Definitions map[span.Span]Definition |
| type Implementations map[span.Span][]span.Span |
| type Highlights map[span.Span][]span.Span |
| type References map[span.Span][]span.Span |
| type Renames map[span.Span]string |
| type PrepareRenames map[span.Span]*source.PrepareItem |
| type Symbols map[span.URI][]protocol.DocumentSymbol |
| type SymbolsChildren map[string][]protocol.DocumentSymbol |
| type SymbolInformation map[span.Span]protocol.SymbolInformation |
| type WorkspaceSymbols map[string][]protocol.SymbolInformation |
| type Signatures map[span.Span]*protocol.SignatureHelp |
| type Links map[span.URI][]Link |
| |
| type Data struct { |
| Config packages.Config |
| Exported *packagestest.Exported |
| CodeLens CodeLens |
| Diagnostics Diagnostics |
| CompletionItems CompletionItems |
| Completions Completions |
| CompletionSnippets CompletionSnippets |
| UnimportedCompletions UnimportedCompletions |
| DeepCompletions DeepCompletions |
| FuzzyCompletions FuzzyCompletions |
| CaseSensitiveCompletions CaseSensitiveCompletions |
| RankCompletions RankCompletions |
| FoldingRanges FoldingRanges |
| Formats Formats |
| Imports Imports |
| SuggestedFixes SuggestedFixes |
| Definitions Definitions |
| Implementations Implementations |
| Highlights Highlights |
| References References |
| Renames Renames |
| PrepareRenames PrepareRenames |
| Symbols Symbols |
| symbolsChildren SymbolsChildren |
| symbolInformation SymbolInformation |
| WorkspaceSymbols WorkspaceSymbols |
| FuzzyWorkspaceSymbols WorkspaceSymbols |
| CaseSensitiveWorkspaceSymbols WorkspaceSymbols |
| Signatures Signatures |
| Links Links |
| |
| t testing.TB |
| fragments map[string]string |
| dir string |
| Folder string |
| golden map[string]*Golden |
| |
| ModfileFlagAvailable bool |
| |
| mappersMu sync.Mutex |
| mappers map[span.URI]*protocol.ColumnMapper |
| } |
| |
| type Tests interface { |
| CodeLens(*testing.T, span.URI, []protocol.CodeLens) |
| Diagnostics(*testing.T, span.URI, []source.Diagnostic) |
| Completion(*testing.T, span.Span, Completion, CompletionItems) |
| CompletionSnippet(*testing.T, span.Span, CompletionSnippet, bool, CompletionItems) |
| UnimportedCompletion(*testing.T, span.Span, Completion, CompletionItems) |
| DeepCompletion(*testing.T, span.Span, Completion, CompletionItems) |
| FuzzyCompletion(*testing.T, span.Span, Completion, CompletionItems) |
| CaseSensitiveCompletion(*testing.T, span.Span, Completion, CompletionItems) |
| RankCompletion(*testing.T, span.Span, Completion, CompletionItems) |
| FoldingRanges(*testing.T, span.Span) |
| Format(*testing.T, span.Span) |
| Import(*testing.T, span.Span) |
| SuggestedFix(*testing.T, span.Span, []string) |
| Definition(*testing.T, span.Span, Definition) |
| Implementation(*testing.T, span.Span, []span.Span) |
| Highlight(*testing.T, span.Span, []span.Span) |
| References(*testing.T, span.Span, []span.Span) |
| Rename(*testing.T, span.Span, string) |
| PrepareRename(*testing.T, span.Span, *source.PrepareItem) |
| Symbols(*testing.T, span.URI, []protocol.DocumentSymbol) |
| WorkspaceSymbols(*testing.T, string, []protocol.SymbolInformation, map[string]struct{}) |
| FuzzyWorkspaceSymbols(*testing.T, string, []protocol.SymbolInformation, map[string]struct{}) |
| CaseSensitiveWorkspaceSymbols(*testing.T, string, []protocol.SymbolInformation, map[string]struct{}) |
| SignatureHelp(*testing.T, span.Span, *protocol.SignatureHelp) |
| Link(*testing.T, span.URI, []Link) |
| } |
| |
| type Definition struct { |
| Name string |
| IsType bool |
| OnlyHover bool |
| Src, Def span.Span |
| } |
| |
| type CompletionTestType int |
| |
| const ( |
| // Default runs the standard completion tests. |
| CompletionDefault = CompletionTestType(iota) |
| |
| // Unimported tests the autocompletion of unimported packages. |
| CompletionUnimported |
| |
| // Deep tests deep completion. |
| CompletionDeep |
| |
| // Fuzzy tests deep completion and fuzzy matching. |
| CompletionFuzzy |
| |
| // CaseSensitive tests case sensitive completion. |
| CompletionCaseSensitive |
| |
| // CompletionRank candidates in test must be valid and in the right relative order. |
| CompletionRank |
| ) |
| |
| type WorkspaceSymbolsTestType int |
| |
| const ( |
| // Default runs the standard workspace symbols tests. |
| WorkspaceSymbolsDefault = WorkspaceSymbolsTestType(iota) |
| |
| // Fuzzy tests workspace symbols with fuzzy matching. |
| WorkspaceSymbolsFuzzy |
| |
| // CaseSensitive tests workspace symbols with case sensitive. |
| WorkspaceSymbolsCaseSensitive |
| ) |
| |
| type Completion struct { |
| CompletionItems []token.Pos |
| } |
| |
| type CompletionSnippet struct { |
| CompletionItem token.Pos |
| PlainSnippet string |
| PlaceholderSnippet string |
| } |
| |
| type Link struct { |
| Src span.Span |
| Target string |
| NotePosition token.Position |
| } |
| |
| type Golden struct { |
| Filename string |
| Archive *txtar.Archive |
| Modified bool |
| } |
| |
| 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: { |
| protocol.SourceOrganizeImports: true, |
| }, |
| source.Sum: {}, |
| } |
| o.HoverKind = source.SynopsisDocumentation |
| o.InsertTextFormat = protocol.SnippetTextFormat |
| o.CompletionBudget = time.Minute |
| o.HierarchicalDocumentSymbolSupport = true |
| return o |
| } |
| |
| var haveCgo = false |
| |
| // For Load() to properly create the folder structure required when testing with modules. |
| // The directory structure of a test needs to look like the example below: |
| // |
| // - dir |
| // - primarymod |
| // - .go files |
| // - packages |
| // - go.mod (optional) |
| // - modules |
| // - repoa |
| // - mod1 |
| // - .go files |
| // - packages |
| // - go.mod (optional) |
| // - mod2 |
| // - repob |
| // - mod1 |
| // |
| // All the files that are primarily being tested should be in the primarymod folder, |
| // any auxillary packages should be declared in the modules folder. |
| // The modules folder requires each module to have the following format: repo/module |
| // Then inside each repo/module, there can be any number of packages and files that are |
| // needed to test the primarymod. |
| func Load(t testing.TB, exporter packagestest.Exporter, dir string) []*Data { |
| t.Helper() |
| |
| folders, err := testFolders(dir) |
| if err != nil { |
| t.Fatalf("could not get test folders for %v, %v", dir, err) |
| } |
| |
| var data []*Data |
| for _, folder := range folders { |
| datum := &Data{ |
| CodeLens: make(CodeLens), |
| Diagnostics: make(Diagnostics), |
| CompletionItems: make(CompletionItems), |
| Completions: make(Completions), |
| CompletionSnippets: make(CompletionSnippets), |
| UnimportedCompletions: make(UnimportedCompletions), |
| DeepCompletions: make(DeepCompletions), |
| FuzzyCompletions: make(FuzzyCompletions), |
| RankCompletions: make(RankCompletions), |
| CaseSensitiveCompletions: make(CaseSensitiveCompletions), |
| Definitions: make(Definitions), |
| Implementations: make(Implementations), |
| Highlights: make(Highlights), |
| References: make(References), |
| Renames: make(Renames), |
| PrepareRenames: make(PrepareRenames), |
| SuggestedFixes: make(SuggestedFixes), |
| Symbols: make(Symbols), |
| symbolsChildren: make(SymbolsChildren), |
| symbolInformation: make(SymbolInformation), |
| WorkspaceSymbols: make(WorkspaceSymbols), |
| FuzzyWorkspaceSymbols: make(WorkspaceSymbols), |
| CaseSensitiveWorkspaceSymbols: make(WorkspaceSymbols), |
| Signatures: make(Signatures), |
| Links: make(Links), |
| |
| t: t, |
| dir: folder, |
| Folder: folder, |
| fragments: map[string]string{}, |
| golden: map[string]*Golden{}, |
| mappers: map[span.URI]*protocol.ColumnMapper{}, |
| } |
| |
| if !*UpdateGolden { |
| summary := filepath.Join(filepath.FromSlash(folder), summaryFile+goldenFileSuffix) |
| if _, err := os.Stat(summary); os.IsNotExist(err) { |
| t.Fatalf("could not find golden file summary.txt in %#v", folder) |
| } |
| archive, err := txtar.ParseFile(summary) |
| if err != nil { |
| t.Fatalf("could not read golden file %v/%v: %v", folder, summary, err) |
| } |
| datum.golden[summaryFile] = &Golden{ |
| Filename: summary, |
| Archive: archive, |
| } |
| } |
| |
| modules, _ := packagestest.GroupFilesByModules(folder) |
| for i, m := range modules { |
| for fragment, operation := range m.Files { |
| if trimmed := strings.TrimSuffix(fragment, goldenFileSuffix); trimmed != fragment { |
| delete(m.Files, fragment) |
| goldFile := filepath.Join(m.Name, fragment) |
| if i == 0 { |
| goldFile = filepath.Join(m.Name, "primarymod", fragment) |
| } |
| archive, err := txtar.ParseFile(goldFile) |
| if err != nil { |
| t.Fatalf("could not read golden file %v: %v", fragment, err) |
| } |
| datum.golden[trimmed] = &Golden{ |
| Filename: goldFile, |
| Archive: archive, |
| } |
| } else if trimmed := strings.TrimSuffix(fragment, inFileSuffix); trimmed != fragment { |
| delete(m.Files, fragment) |
| m.Files[trimmed] = operation |
| } else if index := strings.Index(fragment, overlayFileSuffix); index >= 0 { |
| delete(m.Files, fragment) |
| partial := fragment[:index] + fragment[index+len(overlayFileSuffix):] |
| overlayFile := filepath.Join(m.Name, fragment) |
| if i == 0 { |
| overlayFile = filepath.Join(m.Name, "primarymod", fragment) |
| } |
| contents, err := ioutil.ReadFile(overlayFile) |
| if err != nil { |
| t.Fatal(err) |
| } |
| m.Overlay[partial] = contents |
| } |
| } |
| } |
| if len(modules) > 0 { |
| // For certain LSP related tests to run, make sure that the primary |
| // module for the passed in directory is testModule. |
| modules[0].Name = testModule |
| } |
| // Add exampleModule to provide tests with another pkg. |
| datum.Exported = packagestest.Export(t, exporter, modules) |
| for _, m := range modules { |
| for fragment := range m.Files { |
| filename := datum.Exported.File(m.Name, fragment) |
| datum.fragments[filename] = fragment |
| } |
| } |
| |
| // Turn off go/packages debug logging. |
| datum.Exported.Config.Logf = nil |
| datum.Config.Logf = nil |
| |
| // Merge the exported.Config with the view.Config. |
| datum.Config = *datum.Exported.Config |
| datum.Config.Fset = token.NewFileSet() |
| datum.Config.Context = Context(nil) |
| datum.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 and workspace symbols. |
| if err := datum.Exported.Expect(map[string]interface{}{ |
| "item": func(name string, r packagestest.Range, _ []string) { |
| datum.Exported.Mark(name, r) |
| }, |
| "symbol": func(name string, r packagestest.Range, _ []string) { |
| datum.Exported.Mark(name, r) |
| }, |
| }); err != nil { |
| t.Fatal(err) |
| } |
| |
| // Collect any data that needs to be used by subsequent tests. |
| if err := datum.Exported.Expect(map[string]interface{}{ |
| "codelens": datum.collectCodeLens, |
| "diag": datum.collectDiagnostics, |
| "item": datum.collectCompletionItems, |
| "complete": datum.collectCompletions(CompletionDefault), |
| "unimported": datum.collectCompletions(CompletionUnimported), |
| "deep": datum.collectCompletions(CompletionDeep), |
| "fuzzy": datum.collectCompletions(CompletionFuzzy), |
| "casesensitive": datum.collectCompletions(CompletionCaseSensitive), |
| "rank": datum.collectCompletions(CompletionRank), |
| "snippet": datum.collectCompletionSnippets, |
| "fold": datum.collectFoldingRanges, |
| "format": datum.collectFormats, |
| "import": datum.collectImports, |
| "godef": datum.collectDefinitions, |
| "implementations": datum.collectImplementations, |
| "typdef": datum.collectTypeDefinitions, |
| "hover": datum.collectHoverDefinitions, |
| "highlight": datum.collectHighlights, |
| "refs": datum.collectReferences, |
| "rename": datum.collectRenames, |
| "prepare": datum.collectPrepareRenames, |
| "symbol": datum.collectSymbols, |
| "signature": datum.collectSignatures, |
| "link": datum.collectLinks, |
| "suggestedfix": datum.collectSuggestedFixes, |
| }); err != nil { |
| t.Fatal(err) |
| } |
| for _, symbols := range datum.Symbols { |
| for i := range symbols { |
| children := datum.symbolsChildren[symbols[i].Name] |
| symbols[i].Children = children |
| } |
| } |
| // Collect names for the entries that require golden files. |
| if err := datum.Exported.Expect(map[string]interface{}{ |
| "godef": datum.collectDefinitionNames, |
| "hover": datum.collectDefinitionNames, |
| "workspacesymbol": datum.collectWorkspaceSymbols(WorkspaceSymbolsDefault), |
| "workspacesymbolfuzzy": datum.collectWorkspaceSymbols(WorkspaceSymbolsFuzzy), |
| "workspacesymbolcasesensitive": datum.collectWorkspaceSymbols(WorkspaceSymbolsCaseSensitive), |
| }); err != nil { |
| t.Fatal(err) |
| } |
| data = append(data, datum) |
| } |
| return data |
| } |
| |
| func Run(t *testing.T, tests Tests, data *Data) { |
| t.Helper() |
| checkData(t, data) |
| |
| eachCompletion := func(t *testing.T, cases map[span.Span][]Completion, test func(*testing.T, span.Span, Completion, CompletionItems)) { |
| t.Helper() |
| |
| for src, exp := range cases { |
| for i, e := range exp { |
| t.Run(SpanName(src)+"_"+strconv.Itoa(i), func(t *testing.T) { |
| t.Helper() |
| if (!haveCgo || runtime.GOOS == "android") && strings.Contains(t.Name(), "cgo") { |
| t.Skip("test requires cgo, not supported") |
| } |
| test(t, src, e, data.CompletionItems) |
| }) |
| } |
| |
| } |
| } |
| |
| eachWorkspaceSymbols := func(t *testing.T, cases map[string][]protocol.SymbolInformation, test func(*testing.T, string, []protocol.SymbolInformation, map[string]struct{})) { |
| t.Helper() |
| |
| for query, expectedSymbols := range cases { |
| name := query |
| if name == "" { |
| name = "EmptyQuery" |
| } |
| t.Run(name, func(t *testing.T) { |
| t.Helper() |
| dirs := make(map[string]struct{}) |
| for _, si := range expectedSymbols { |
| d := filepath.Dir(si.Location.URI.SpanURI().Filename()) |
| if _, ok := dirs[d]; !ok { |
| dirs[d] = struct{}{} |
| } |
| } |
| test(t, query, expectedSymbols, dirs) |
| }) |
| } |
| } |
| |
| t.Run("Completion", func(t *testing.T) { |
| t.Helper() |
| eachCompletion(t, data.Completions, tests.Completion) |
| }) |
| |
| t.Run("CompletionSnippets", func(t *testing.T) { |
| t.Helper() |
| for _, placeholders := range []bool{true, false} { |
| for src, expecteds := range data.CompletionSnippets { |
| for i, expected := range expecteds { |
| name := SpanName(src) + "_" + strconv.Itoa(i+1) |
| if placeholders { |
| name += "_placeholders" |
| } |
| |
| t.Run(name, func(t *testing.T) { |
| t.Helper() |
| tests.CompletionSnippet(t, src, expected, placeholders, data.CompletionItems) |
| }) |
| } |
| } |
| } |
| }) |
| |
| t.Run("UnimportedCompletion", func(t *testing.T) { |
| t.Helper() |
| eachCompletion(t, data.UnimportedCompletions, tests.UnimportedCompletion) |
| }) |
| |
| t.Run("DeepCompletion", func(t *testing.T) { |
| t.Helper() |
| eachCompletion(t, data.DeepCompletions, tests.DeepCompletion) |
| }) |
| |
| t.Run("FuzzyCompletion", func(t *testing.T) { |
| t.Helper() |
| eachCompletion(t, data.FuzzyCompletions, tests.FuzzyCompletion) |
| }) |
| |
| t.Run("CaseSensitiveCompletion", func(t *testing.T) { |
| t.Helper() |
| eachCompletion(t, data.CaseSensitiveCompletions, tests.CaseSensitiveCompletion) |
| }) |
| |
| t.Run("RankCompletions", func(t *testing.T) { |
| t.Helper() |
| eachCompletion(t, data.RankCompletions, tests.RankCompletion) |
| }) |
| |
| t.Run("CodeLens", func(t *testing.T) { |
| t.Helper() |
| for uri, want := range data.CodeLens { |
| // Check if we should skip this URI if the -modfile flag is not available. |
| if shouldSkip(data, uri) { |
| continue |
| } |
| t.Run(uriName(uri), func(t *testing.T) { |
| t.Helper() |
| tests.CodeLens(t, uri, want) |
| }) |
| } |
| }) |
| |
| t.Run("Diagnostics", func(t *testing.T) { |
| t.Helper() |
| for uri, want := range data.Diagnostics { |
| // Check if we should skip this URI if the -modfile flag is not available. |
| if shouldSkip(data, uri) { |
| continue |
| } |
| t.Run(uriName(uri), func(t *testing.T) { |
| t.Helper() |
| tests.Diagnostics(t, uri, want) |
| }) |
| } |
| }) |
| |
| t.Run("FoldingRange", func(t *testing.T) { |
| t.Helper() |
| for _, spn := range data.FoldingRanges { |
| t.Run(uriName(spn.URI()), func(t *testing.T) { |
| t.Helper() |
| tests.FoldingRanges(t, spn) |
| }) |
| } |
| }) |
| |
| t.Run("Format", func(t *testing.T) { |
| t.Helper() |
| for _, spn := range data.Formats { |
| t.Run(uriName(spn.URI()), func(t *testing.T) { |
| t.Helper() |
| tests.Format(t, spn) |
| }) |
| } |
| }) |
| |
| t.Run("Import", func(t *testing.T) { |
| t.Helper() |
| for _, spn := range data.Imports { |
| t.Run(uriName(spn.URI()), func(t *testing.T) { |
| t.Helper() |
| tests.Import(t, spn) |
| }) |
| } |
| }) |
| |
| t.Run("SuggestedFix", func(t *testing.T) { |
| t.Helper() |
| for spn, actionKinds := range data.SuggestedFixes { |
| // Check if we should skip this spn if the -modfile flag is not available. |
| if shouldSkip(data, spn.URI()) { |
| continue |
| } |
| t.Run(SpanName(spn), func(t *testing.T) { |
| t.Helper() |
| tests.SuggestedFix(t, spn, actionKinds) |
| }) |
| } |
| }) |
| |
| t.Run("Definition", func(t *testing.T) { |
| t.Helper() |
| for spn, d := range data.Definitions { |
| t.Run(SpanName(spn), func(t *testing.T) { |
| t.Helper() |
| if (!haveCgo || runtime.GOOS == "android") && strings.Contains(t.Name(), "cgo") { |
| t.Skip("test requires cgo, not supported") |
| } |
| tests.Definition(t, spn, d) |
| }) |
| } |
| }) |
| |
| t.Run("Implementation", func(t *testing.T) { |
| t.Helper() |
| for spn, m := range data.Implementations { |
| t.Run(SpanName(spn), func(t *testing.T) { |
| t.Helper() |
| tests.Implementation(t, spn, m) |
| }) |
| } |
| }) |
| |
| t.Run("Highlight", func(t *testing.T) { |
| t.Helper() |
| for pos, locations := range data.Highlights { |
| t.Run(SpanName(pos), func(t *testing.T) { |
| t.Helper() |
| tests.Highlight(t, pos, locations) |
| }) |
| } |
| }) |
| |
| t.Run("References", func(t *testing.T) { |
| t.Helper() |
| for src, itemList := range data.References { |
| t.Run(SpanName(src), func(t *testing.T) { |
| t.Helper() |
| tests.References(t, src, itemList) |
| }) |
| } |
| }) |
| |
| t.Run("Renames", func(t *testing.T) { |
| t.Helper() |
| for spn, newText := range data.Renames { |
| t.Run(uriName(spn.URI())+"_"+newText, func(t *testing.T) { |
| t.Helper() |
| tests.Rename(t, spn, newText) |
| }) |
| } |
| }) |
| |
| t.Run("PrepareRenames", func(t *testing.T) { |
| t.Helper() |
| for src, want := range data.PrepareRenames { |
| t.Run(SpanName(src), func(t *testing.T) { |
| t.Helper() |
| tests.PrepareRename(t, src, want) |
| }) |
| } |
| }) |
| |
| t.Run("Symbols", func(t *testing.T) { |
| t.Helper() |
| for uri, expectedSymbols := range data.Symbols { |
| t.Run(uriName(uri), func(t *testing.T) { |
| t.Helper() |
| tests.Symbols(t, uri, expectedSymbols) |
| }) |
| } |
| }) |
| |
| t.Run("WorkspaceSymbols", func(t *testing.T) { |
| t.Helper() |
| eachWorkspaceSymbols(t, data.WorkspaceSymbols, tests.WorkspaceSymbols) |
| }) |
| |
| t.Run("FuzzyWorkspaceSymbols", func(t *testing.T) { |
| t.Helper() |
| eachWorkspaceSymbols(t, data.FuzzyWorkspaceSymbols, tests.FuzzyWorkspaceSymbols) |
| }) |
| |
| t.Run("CaseSensitiveWorkspaceSymbols", func(t *testing.T) { |
| t.Helper() |
| eachWorkspaceSymbols(t, data.CaseSensitiveWorkspaceSymbols, tests.CaseSensitiveWorkspaceSymbols) |
| }) |
| |
| t.Run("SignatureHelp", func(t *testing.T) { |
| t.Helper() |
| for spn, expectedSignature := range data.Signatures { |
| t.Run(SpanName(spn), func(t *testing.T) { |
| t.Helper() |
| tests.SignatureHelp(t, spn, expectedSignature) |
| }) |
| } |
| }) |
| |
| t.Run("Link", func(t *testing.T) { |
| t.Helper() |
| for uri, wantLinks := range data.Links { |
| // If we are testing GOPATH, then we do not want links with |
| // the versions attached (pkg.go.dev/repoa/moda@v1.1.0/pkg), |
| // unless the file is a go.mod, then we can skip it alltogether. |
| if data.Exported.Exporter == packagestest.GOPATH { |
| if strings.HasSuffix(uri.Filename(), ".mod") { |
| continue |
| } |
| re := regexp.MustCompile(`@v\d+\.\d+\.[\w-]+`) |
| for i, link := range wantLinks { |
| wantLinks[i].Target = re.ReplaceAllString(link.Target, "") |
| } |
| } |
| t.Run(uriName(uri), func(t *testing.T) { |
| t.Helper() |
| tests.Link(t, uri, wantLinks) |
| }) |
| } |
| }) |
| |
| 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 checkData(t *testing.T, data *Data) { |
| buf := &bytes.Buffer{} |
| diagnosticsCount := 0 |
| for _, want := range data.Diagnostics { |
| diagnosticsCount += len(want) |
| } |
| linksCount := 0 |
| for _, want := range data.Links { |
| linksCount += len(want) |
| } |
| definitionCount := 0 |
| typeDefinitionCount := 0 |
| for _, d := range data.Definitions { |
| if d.IsType { |
| typeDefinitionCount++ |
| } else { |
| definitionCount++ |
| } |
| } |
| |
| snippetCount := 0 |
| for _, want := range data.CompletionSnippets { |
| snippetCount += len(want) |
| } |
| |
| countCompletions := func(c map[span.Span][]Completion) (count int) { |
| for _, want := range c { |
| count += len(want) |
| } |
| return count |
| } |
| |
| countCodeLens := func(c map[span.URI][]protocol.CodeLens) (count int) { |
| for _, want := range c { |
| count += len(want) |
| } |
| return count |
| } |
| |
| fmt.Fprintf(buf, "CodeLensCount = %v\n", countCodeLens(data.CodeLens)) |
| fmt.Fprintf(buf, "CompletionsCount = %v\n", countCompletions(data.Completions)) |
| fmt.Fprintf(buf, "CompletionSnippetCount = %v\n", snippetCount) |
| fmt.Fprintf(buf, "UnimportedCompletionsCount = %v\n", countCompletions(data.UnimportedCompletions)) |
| fmt.Fprintf(buf, "DeepCompletionsCount = %v\n", countCompletions(data.DeepCompletions)) |
| fmt.Fprintf(buf, "FuzzyCompletionsCount = %v\n", countCompletions(data.FuzzyCompletions)) |
| fmt.Fprintf(buf, "RankedCompletionsCount = %v\n", countCompletions(data.RankCompletions)) |
| fmt.Fprintf(buf, "CaseSensitiveCompletionsCount = %v\n", countCompletions(data.CaseSensitiveCompletions)) |
| fmt.Fprintf(buf, "DiagnosticsCount = %v\n", diagnosticsCount) |
| fmt.Fprintf(buf, "FoldingRangesCount = %v\n", len(data.FoldingRanges)) |
| fmt.Fprintf(buf, "FormatCount = %v\n", len(data.Formats)) |
| fmt.Fprintf(buf, "ImportCount = %v\n", len(data.Imports)) |
| fmt.Fprintf(buf, "SuggestedFixCount = %v\n", len(data.SuggestedFixes)) |
| fmt.Fprintf(buf, "DefinitionsCount = %v\n", definitionCount) |
| fmt.Fprintf(buf, "TypeDefinitionsCount = %v\n", typeDefinitionCount) |
| fmt.Fprintf(buf, "HighlightsCount = %v\n", len(data.Highlights)) |
| fmt.Fprintf(buf, "ReferencesCount = %v\n", len(data.References)) |
| fmt.Fprintf(buf, "RenamesCount = %v\n", len(data.Renames)) |
| fmt.Fprintf(buf, "PrepareRenamesCount = %v\n", len(data.PrepareRenames)) |
| fmt.Fprintf(buf, "SymbolsCount = %v\n", len(data.Symbols)) |
| fmt.Fprintf(buf, "WorkspaceSymbolsCount = %v\n", len(data.WorkspaceSymbols)) |
| fmt.Fprintf(buf, "FuzzyWorkspaceSymbolsCount = %v\n", len(data.FuzzyWorkspaceSymbols)) |
| fmt.Fprintf(buf, "CaseSensitiveWorkspaceSymbolsCount = %v\n", len(data.CaseSensitiveWorkspaceSymbols)) |
| fmt.Fprintf(buf, "SignaturesCount = %v\n", len(data.Signatures)) |
| fmt.Fprintf(buf, "LinksCount = %v\n", linksCount) |
| fmt.Fprintf(buf, "ImplementationsCount = %v\n", len(data.Implementations)) |
| |
| want := string(data.Golden("summary", summaryFile, func() ([]byte, error) { |
| return buf.Bytes(), nil |
| })) |
| got := buf.String() |
| if want != got { |
| t.Errorf("test summary does not match, want\n%s\ngot:\n%s", want, got) |
| } |
| } |
| |
| func (data *Data) Mapper(uri span.URI) (*protocol.ColumnMapper, error) { |
| data.mappersMu.Lock() |
| defer data.mappersMu.Unlock() |
| |
| if _, ok := data.mappers[uri]; !ok { |
| content, err := data.Exported.FileContents(uri.Filename()) |
| if err != nil { |
| return nil, err |
| } |
| converter := span.NewContentConverter(uri.Filename(), content) |
| data.mappers[uri] = &protocol.ColumnMapper{ |
| URI: uri, |
| Converter: converter, |
| Content: content, |
| } |
| } |
| return data.mappers[uri], nil |
| } |
| |
| 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) |
| } |
| var subdir string |
| if fragment != summaryFile { |
| subdir = "primarymod" |
| } |
| golden = &Golden{ |
| Filename: filepath.Join(data.dir, subdir, 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) collectCodeLens(spn span.Span, title, cmd string) { |
| if _, ok := data.CodeLens[spn.URI()]; !ok { |
| data.CodeLens[spn.URI()] = []protocol.CodeLens{} |
| } |
| m, err := data.Mapper(spn.URI()) |
| if err != nil { |
| return |
| } |
| rng, err := m.Range(spn) |
| if err != nil { |
| return |
| } |
| data.CodeLens[spn.URI()] = append(data.CodeLens[spn.URI()], protocol.CodeLens{ |
| Range: rng, |
| Command: protocol.Command{ |
| Title: title, |
| Command: cmd, |
| }, |
| }) |
| } |
| |
| func (data *Data) collectDiagnostics(spn span.Span, msgSource, msg, msgSeverity string) { |
| if _, ok := data.Diagnostics[spn.URI()]; !ok { |
| data.Diagnostics[spn.URI()] = []source.Diagnostic{} |
| } |
| m, err := data.Mapper(spn.URI()) |
| if err != nil { |
| return |
| } |
| rng, err := m.Range(spn) |
| if err != nil { |
| return |
| } |
| severity := protocol.SeverityError |
| switch msgSeverity { |
| case "error": |
| severity = protocol.SeverityError |
| case "warning": |
| severity = protocol.SeverityWarning |
| case "hint": |
| severity = protocol.SeverityHint |
| case "information": |
| severity = protocol.SeverityInformation |
| } |
| // This is not the correct way to do this, but it seems excessive to do the full conversion here. |
| want := source.Diagnostic{ |
| Range: rng, |
| Severity: severity, |
| Source: msgSource, |
| Message: msg, |
| } |
| data.Diagnostics[spn.URI()] = append(data.Diagnostics[spn.URI()], want) |
| } |
| |
| func (data *Data) collectCompletions(typ CompletionTestType) func(span.Span, []token.Pos) { |
| result := func(m map[span.Span][]Completion, src span.Span, expected []token.Pos) { |
| m[src] = append(m[src], Completion{ |
| CompletionItems: expected, |
| }) |
| } |
| 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) |
| } |
| case CompletionCaseSensitive: |
| return func(src span.Span, expected []token.Pos) { |
| result(data.CaseSensitiveCompletions, src, expected) |
| } |
| default: |
| return func(src span.Span, expected []token.Pos) { |
| result(data.Completions, src, expected) |
| } |
| } |
| } |
| |
| func (data *Data) collectCompletionItems(pos token.Pos, args []string) { |
| if len(args) < 3 { |
| loc := data.Exported.ExpectFileSet.Position(pos) |
| data.t.Fatalf("%s:%d: @item expects at least 3 args, got %d", |
| loc.Filename, loc.Line, len(args)) |
| } |
| label, detail, kind := args[0], args[1], args[2] |
| var documentation string |
| if len(args) == 4 { |
| documentation = args[3] |
| } |
| data.CompletionItems[pos] = &source.CompletionItem{ |
| Label: label, |
| Detail: detail, |
| Kind: protocol.ParseCompletionItemKind(kind), |
| Documentation: documentation, |
| } |
| } |
| |
| func (data *Data) collectFoldingRanges(spn span.Span) { |
| data.FoldingRanges = append(data.FoldingRanges, spn) |
| } |
| |
| 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) collectSuggestedFixes(spn span.Span, actionKind string) { |
| if _, ok := data.SuggestedFixes[spn]; !ok { |
| data.SuggestedFixes[spn] = []string{} |
| } |
| data.SuggestedFixes[spn] = append(data.SuggestedFixes[spn], actionKind) |
| } |
| |
| func (data *Data) collectDefinitions(src, target span.Span) { |
| data.Definitions[src] = Definition{ |
| Src: src, |
| Def: target, |
| } |
| } |
| |
| func (data *Data) collectImplementations(src span.Span, targets []span.Span) { |
| data.Implementations[src] = targets |
| } |
| |
| 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(src span.Span, expected []span.Span) { |
| // Declaring a highlight in a test file: @highlight(src, expected1, expected2) |
| data.Highlights[src] = append(data.Highlights[src], expected...) |
| } |
| |
| 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) collectPrepareRenames(src span.Span, rng span.Range, placeholder string) { |
| m, err := data.Mapper(src.URI()) |
| if err != nil { |
| data.t.Fatal(err) |
| } |
| // Convert range to span and then to protocol.Range. |
| spn, err := rng.Span() |
| if err != nil { |
| data.t.Fatal(err) |
| } |
| prng, err := m.Range(spn) |
| if err != nil { |
| data.t.Fatal(err) |
| } |
| data.PrepareRenames[src] = &source.PrepareItem{ |
| Range: prng, |
| Text: placeholder, |
| } |
| } |
| |
| func (data *Data) collectSymbols(name string, spn span.Span, kind string, parentName string) { |
| m, err := data.Mapper(spn.URI()) |
| if err != nil { |
| data.t.Fatal(err) |
| } |
| rng, err := m.Range(spn) |
| if err != nil { |
| data.t.Fatal(err) |
| } |
| sym := protocol.DocumentSymbol{ |
| Name: name, |
| Kind: protocol.ParseSymbolKind(kind), |
| SelectionRange: rng, |
| } |
| if parentName == "" { |
| data.Symbols[spn.URI()] = append(data.Symbols[spn.URI()], sym) |
| } else { |
| data.symbolsChildren[parentName] = append(data.symbolsChildren[parentName], sym) |
| } |
| |
| // Reuse @symbol in the workspace symbols tests. |
| si := protocol.SymbolInformation{ |
| Name: sym.Name, |
| Kind: sym.Kind, |
| Location: protocol.Location{ |
| URI: protocol.URIFromSpanURI(spn.URI()), |
| Range: sym.SelectionRange, |
| }, |
| } |
| data.symbolInformation[spn] = si |
| } |
| |
| func (data *Data) collectWorkspaceSymbols(typ WorkspaceSymbolsTestType) func(string, []span.Span) { |
| switch typ { |
| case WorkspaceSymbolsFuzzy: |
| return func(query string, targets []span.Span) { |
| data.FuzzyWorkspaceSymbols[query] = make([]protocol.SymbolInformation, 0, len(targets)) |
| for _, target := range targets { |
| data.FuzzyWorkspaceSymbols[query] = append(data.FuzzyWorkspaceSymbols[query], data.symbolInformation[target]) |
| } |
| } |
| case WorkspaceSymbolsCaseSensitive: |
| return func(query string, targets []span.Span) { |
| data.CaseSensitiveWorkspaceSymbols[query] = make([]protocol.SymbolInformation, 0, len(targets)) |
| for _, target := range targets { |
| data.CaseSensitiveWorkspaceSymbols[query] = append(data.CaseSensitiveWorkspaceSymbols[query], data.symbolInformation[target]) |
| } |
| } |
| default: |
| return func(query string, targets []span.Span) { |
| data.WorkspaceSymbols[query] = make([]protocol.SymbolInformation, 0, len(targets)) |
| for _, target := range targets { |
| data.WorkspaceSymbols[query] = append(data.WorkspaceSymbols[query], data.symbolInformation[target]) |
| } |
| } |
| } |
| } |
| |
| func (data *Data) collectSignatures(spn span.Span, signature string, activeParam int64) { |
| data.Signatures[spn] = &protocol.SignatureHelp{ |
| Signatures: []protocol.SignatureInformation{ |
| { |
| Label: signature, |
| }, |
| }, |
| ActiveParameter: float64(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] = append(data.CompletionSnippets[spn], CompletionSnippet{ |
| CompletionItem: item, |
| PlainSnippet: plain, |
| PlaceholderSnippet: placeholder, |
| }) |
| } |
| |
| func (data *Data) collectLinks(spn span.Span, link string, note *expect.Note, fset *token.FileSet) { |
| position := fset.Position(note.Pos) |
| uri := spn.URI() |
| data.Links[uri] = append(data.Links[uri], Link{ |
| Src: spn, |
| Target: link, |
| NotePosition: position, |
| }) |
| } |
| |
| func uriName(uri span.URI) string { |
| return filepath.Base(strings.TrimSuffix(uri.Filename(), ".go")) |
| } |
| |
| func SpanName(spn span.Span) string { |
| return fmt.Sprintf("%v_%v_%v", uriName(spn.URI()), spn.Start().Line(), spn.Start().Column()) |
| } |
| |
| func CopyFolderToTempDir(folder string) (string, error) { |
| if _, err := os.Stat(folder); err != nil { |
| return "", err |
| } |
| dst, err := ioutil.TempDir("", "modfile_test") |
| if err != nil { |
| return "", err |
| } |
| fds, err := ioutil.ReadDir(folder) |
| if err != nil { |
| return "", err |
| } |
| for _, fd := range fds { |
| srcfp := filepath.Join(folder, fd.Name()) |
| stat, err := os.Stat(srcfp) |
| if err != nil { |
| return "", err |
| } |
| if !stat.Mode().IsRegular() { |
| return "", fmt.Errorf("cannot copy non regular file %s", srcfp) |
| } |
| contents, err := ioutil.ReadFile(srcfp) |
| if err != nil { |
| return "", err |
| } |
| if err := ioutil.WriteFile(filepath.Join(dst, fd.Name()), contents, stat.Mode()); err != nil { |
| return "", err |
| } |
| } |
| return dst, nil |
| } |
| |
| func testFolders(root string) ([]string, error) { |
| // Check if this only has one test directory. |
| if _, err := os.Stat(filepath.Join(filepath.FromSlash(root), "primarymod")); !os.IsNotExist(err) { |
| return []string{root}, nil |
| } |
| folders := []string{} |
| root = filepath.FromSlash(root) |
| // Get all test directories that are one level deeper than root. |
| if err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error { |
| if !info.IsDir() { |
| return nil |
| } |
| if filepath.Dir(path) == root { |
| folders = append(folders, filepath.ToSlash(path)) |
| } |
| return nil |
| }); err != nil { |
| return nil, err |
| } |
| return folders, nil |
| } |
| |
| func shouldSkip(data *Data, uri span.URI) bool { |
| if data.ModfileFlagAvailable { |
| return false |
| } |
| // If the -modfile flag is not available, then we do not want to run |
| // any tests on the go.mod file. |
| if strings.HasSuffix(uri.Filename(), ".mod") { |
| return true |
| } |
| // If the -modfile flag is not available, then we do not want to test any |
| // uri that contains "go mod tidy". |
| m, err := data.Mapper(uri) |
| return err == nil && strings.Contains(string(m.Content), ", \"go mod tidy\",") |
| } |