// 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" "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/lsp/source/completion" "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/testenv" "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 CallHierarchy map[span.Span]*CallHierarchyResult type CodeLens map[span.URI][]protocol.CodeLens type Diagnostics map[span.URI][]*source.Diagnostic type CompletionItems map[token.Pos]*completion.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 SemanticTokens []span.Span type SuggestedFixes map[span.Span][]string type FunctionExtractions map[span.Span]span.Span 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 CallHierarchy CallHierarchy 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 SemanticTokens SemanticTokens SuggestedFixes SuggestedFixes FunctionExtractions FunctionExtractions 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 golden map[string]*Golden mode string ModfileFlagAvailable bool mappersMu sync.Mutex mappers map[span.URI]*protocol.ColumnMapper } type Tests interface { CallHierarchy(*testing.T, span.Span, *CallHierarchyResult) 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) SemanticTokens(*testing.T, span.Span) SuggestedFix(*testing.T, span.Span, []string) FunctionExtraction(*testing.T, span.Span, span.Span) 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 CallHierarchyResult struct { IncomingCalls, OutgoingCalls []protocol.CallHierarchyItem } 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(o *source.Options) { o.SupportedCodeActions = map[source.FileKind]map[protocol.CodeActionKind]bool{ source.Go: { protocol.SourceOrganizeImports: true, protocol.QuickFix: true, protocol.RefactorRewrite: true, protocol.RefactorExtract: true, protocol.SourceFixAll: true, }, source.Mod: { protocol.SourceOrganizeImports: true, }, source.Sum: {}, } o.ExperimentalOptions.Codelens[source.CommandTest.Name] = true o.HoverKind = source.SynopsisDocumentation o.InsertTextFormat = protocol.SnippetTextFormat o.CompletionBudget = time.Minute o.HierarchicalDocumentSymbolSupport = true o.ExperimentalWorkspaceModule = true o.SemanticTokens = true } func RunTests(t *testing.T, dataDir string, includeMultiModule bool, f func(*testing.T, *Data)) { t.Helper() modes := []string{"Modules", "GOPATH"} if includeMultiModule { modes = append(modes, "MultiModule") } for _, mode := range modes { t.Run(mode, func(t *testing.T) { t.Helper() if mode == "MultiModule" { // Some bug in 1.12 breaks reading markers, and it's not worth figuring out. testenv.NeedsGo1Point(t, 13) } datum := load(t, mode, dataDir) f(t, datum) }) } } func load(t testing.TB, mode string, dir string) *Data { t.Helper() datum := &Data{ CallHierarchy: make(CallHierarchy), 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), FunctionExtractions: make(FunctionExtractions), 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: dir, fragments: map[string]string{}, golden: map[string]*Golden{}, mode: mode, mappers: map[span.URI]*protocol.ColumnMapper{}, } if !*UpdateGolden { summary := filepath.Join(filepath.FromSlash(dir), summaryFile+goldenFileSuffix) if _, err := os.Stat(summary); os.IsNotExist(err) { t.Fatalf("could not find golden file summary.txt in %#v", dir) } archive, err := txtar.ParseFile(summary) if err != nil { t.Fatalf("could not read golden file %v/%v: %v", dir, summary, err) } datum.golden[summaryFile] = &Golden{ Filename: summary, Archive: archive, } } 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) } datum.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, }, } switch mode { case "Modules": datum.Exported = packagestest.Export(t, packagestest.Modules, modules) case "GOPATH": datum.Exported = packagestest.Export(t, packagestest.GOPATH, modules) case "MultiModule": files := map[string]interface{}{} for k, v := range modules[0].Files { files[filepath.Join("testmodule", k)] = v } modules[0].Files = files overlays := map[string][]byte{} for k, v := range modules[0].Overlay { overlays[filepath.Join("testmodule", k)] = v } modules[0].Overlay = overlays golden := map[string]*Golden{} for k, v := range datum.golden { if k == summaryFile { golden[k] = v } else { golden[filepath.Join("testmodule", k)] = v } } datum.golden = golden datum.Exported = packagestest.Export(t, packagestest.Modules, modules) default: panic("unknown mode " + mode) } 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, "semantic": datum.collectSemanticTokens, "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, "extractfunc": datum.collectFunctionExtractions, "incomingcalls": datum.collectIncomingCalls, "outgoingcalls": datum.collectOutgoingCalls, }); 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) } if mode == "MultiModule" { if err := os.Rename(filepath.Join(datum.Config.Dir, "go.mod"), filepath.Join(datum.Config.Dir, "testmodule/go.mod")); err != nil { t.Fatal(err) } } return datum } 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 strings.Contains(t.Name(), "complit") || strings.Contains(t.Name(), "UnimportedCompletion") { if data.mode == "MultiModule" { t.Skip("Unimported completions are broken in multi-module mode") } } if strings.Contains(t.Name(), "cgo") { testenv.NeedsTool(t, "cgo") } if strings.Contains(t.Name(), "declarecgo") { testenv.NeedsGo1Point(t, 15) } 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("CallHierarchy", func(t *testing.T) { t.Helper() for spn, callHierarchyResult := range data.CallHierarchy { t.Run(SpanName(spn), func(t *testing.T) { t.Helper() tests.CallHierarchy(t, spn, callHierarchyResult) }) } }) 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("SemanticTokens", func(t *testing.T) { t.Helper() for _, spn := range data.SemanticTokens { t.Run(uriName(spn.URI()), func(t *testing.T) { t.Helper() tests.SemanticTokens(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("FunctionExtraction", func(t *testing.T) { t.Helper() for start, end := range data.FunctionExtractions { // Check if we should skip this spn if the -modfile flag is not available. if shouldSkip(data, start.URI()) { continue } t.Run(SpanName(start), func(t *testing.T) { t.Helper() tests.FunctionExtraction(t, start, end) }) } }) 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 strings.Contains(t.Name(), "cgo") { testenv.NeedsTool(t, "cgo") } if strings.Contains(t.Name(), "declarecgo") { testenv.NeedsGo1Point(t, 15) } 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, "CallHierarchyCount = %v\n", len(data.CallHierarchy)) 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, "SemanticTokenCount = %v\n", len(data.SemanticTokens)) fmt.Fprintf(buf, "SuggestedFixCount = %v\n", len(data.SuggestedFixes)) fmt.Fprintf(buf, "FunctionExtractionCount = %v\n", len(data.FunctionExtractions)) 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:\n%s", Diff(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) } if len(file.Data) == 0 { return file.Data } 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] = &completion.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) collectSemanticTokens(spn span.Span) { data.SemanticTokens = append(data.SemanticTokens, 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) collectFunctionExtractions(start span.Span, end span.Span) { if _, ok := data.FunctionExtractions[start]; !ok { data.FunctionExtractions[start] = end } } 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) collectIncomingCalls(src span.Span, calls []span.Span) { for _, call := range calls { m, err := data.Mapper(call.URI()) if err != nil { data.t.Fatal(err) } rng, err := m.Range(call) if err != nil { data.t.Fatal(err) } // we're only comparing protocol.range if data.CallHierarchy[src] != nil { data.CallHierarchy[src].IncomingCalls = append(data.CallHierarchy[src].IncomingCalls, protocol.CallHierarchyItem{ URI: protocol.DocumentURI(call.URI()), Range: rng, }) } else { data.CallHierarchy[src] = &CallHierarchyResult{ IncomingCalls: []protocol.CallHierarchyItem{ {URI: protocol.DocumentURI(call.URI()), Range: rng}, }, } } } } func (data *Data) collectOutgoingCalls(src span.Span, calls []span.Span) { for _, call := range calls { m, err := data.Mapper(call.URI()) if err != nil { data.t.Fatal(err) } rng, err := m.Range(call) if err != nil { data.t.Fatal(err) } // we're only comparing protocol.range if data.CallHierarchy[src] != nil { data.CallHierarchy[src].OutgoingCalls = append(data.CallHierarchy[src].OutgoingCalls, protocol.CallHierarchyItem{ URI: protocol.DocumentURI(call.URI()), Range: rng, }) } else { data.CallHierarchy[src] = &CallHierarchyResult{ OutgoingCalls: []protocol.CallHierarchyItem{ {URI: protocol.DocumentURI(call.URI()), Range: rng}, }, } } } } 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, } } // collectSymbols is responsible for collecting @symbol annotations. func (data *Data) collectSymbols(name string, spn span.Span, kind string, parentName string, siName 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: siName, 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 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\",") }