From 4479ae8d8ce900e28e26c8651ce72fa5eda4a0d5 Mon Sep 17 00:00:00 2001 From: Rob Findley Date: Thu, 9 Nov 2023 15:46:31 -0500 Subject: [PATCH] gopls/internal/regtest/marker: port remaining marker tests Port the call hierarchy tests to the new framework. With this change, all marker tests have been ported, and the old test harness can be deleted. Fixes golang/go#54845 Change-Id: I607c6cdef7029d1a8bbb9509d03700924f81cbea Reviewed-on: https://go-review.googlesource.com/c/tools/+/541236 Auto-Submit: Robert Findley Reviewed-by: Alan Donovan LUCI-TryBot-Result: Go LUCI --- gopls/internal/lsp/lsp_test.go | 235 -------- gopls/internal/lsp/regtest/marker.go | 106 +++- gopls/internal/lsp/reset_golden.sh | 30 - .../testdata/callhierarchy/callhierarchy.go | 70 --- .../callhierarchy/incoming/incoming.go | 12 - .../callhierarchy/outgoing/outgoing.go | 9 - .../internal/lsp/testdata/summary.txt.golden | 3 - gopls/internal/lsp/tests/README.md | 66 --- gopls/internal/lsp/tests/tests.go | 512 ------------------ gopls/internal/lsp/tests/util.go | 33 -- .../testdata/callhierarchy/callhierarchy.txt | 94 ++++ 11 files changed, 192 insertions(+), 978 deletions(-) delete mode 100644 gopls/internal/lsp/lsp_test.go delete mode 100755 gopls/internal/lsp/reset_golden.sh delete mode 100644 gopls/internal/lsp/testdata/callhierarchy/callhierarchy.go delete mode 100644 gopls/internal/lsp/testdata/callhierarchy/incoming/incoming.go delete mode 100644 gopls/internal/lsp/testdata/callhierarchy/outgoing/outgoing.go delete mode 100644 gopls/internal/lsp/testdata/summary.txt.golden delete mode 100644 gopls/internal/lsp/tests/README.md delete mode 100644 gopls/internal/lsp/tests/tests.go delete mode 100644 gopls/internal/lsp/tests/util.go create mode 100644 gopls/internal/regtest/marker/testdata/callhierarchy/callhierarchy.txt diff --git a/gopls/internal/lsp/lsp_test.go b/gopls/internal/lsp/lsp_test.go deleted file mode 100644 index 21f219c2688..00000000000 --- a/gopls/internal/lsp/lsp_test.go +++ /dev/null @@ -1,235 +0,0 @@ -// Copyright 2018 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package lsp - -import ( - "context" - "os" - "path/filepath" - "testing" - - "golang.org/x/tools/gopls/internal/bug" - "golang.org/x/tools/gopls/internal/lsp/cache" - "golang.org/x/tools/gopls/internal/lsp/debug" - "golang.org/x/tools/gopls/internal/lsp/protocol" - "golang.org/x/tools/gopls/internal/lsp/source" - "golang.org/x/tools/gopls/internal/lsp/tests" - "golang.org/x/tools/gopls/internal/span" - "golang.org/x/tools/internal/testenv" -) - -func TestMain(m *testing.M) { - bug.PanicOnBugs = true - testenv.ExitIfSmallMachine() - - os.Exit(m.Run()) -} - -// TestLSP runs the marker tests in files beneath testdata/ using -// implementations of each of the marker operations that make LSP RPCs to a -// gopls server. -func TestLSP(t *testing.T) { - tests.RunTests(t, "testdata", true, testLSP) -} - -func testLSP(t *testing.T, datum *tests.Data) { - ctx := tests.Context(t) - - // Setting a debug instance suppresses logging to stderr, but ensures that we - // still e.g. convert events into runtime/trace/instrumentation. - // - // Previously, we called event.SetExporter(nil), which turns off all - // instrumentation. - ctx = debug.WithInstance(ctx, "", "off") - - session := cache.NewSession(ctx, cache.New(nil)) - options := source.DefaultOptions(tests.DefaultOptions) - options.SetEnvSlice(datum.Config.Env) - folder := &cache.Folder{ - Dir: span.URIFromPath(datum.Config.Dir), - Name: datum.Config.Dir, - Options: options, - } - view, snapshot, release, err := session.NewView(ctx, folder) - if err != nil { - t.Fatal(err) - } - - defer session.RemoveView(view) - - // Only run the -modfile specific tests in module mode with Go 1.14 or above. - datum.ModfileFlagAvailable = len(snapshot.ModFiles()) > 0 && testenv.Go1Point() >= 14 - release() - - // Open all files for performance reasons, because gopls only - // keeps active packages (those with open files) in memory. - // - // In practice clients will only send document-oriented requests for open - // files. - var modifications []source.FileModification - for _, module := range datum.Exported.Modules { - for name := range module.Files { - filename := datum.Exported.File(module.Name, name) - if filepath.Ext(filename) != ".go" { - continue - } - content, err := datum.Exported.FileContents(filename) - if err != nil { - t.Fatal(err) - } - modifications = append(modifications, source.FileModification{ - URI: span.URIFromPath(filename), - Action: source.Open, - Version: -1, - Text: content, - LanguageID: "go", - }) - } - } - for filename, content := range datum.Config.Overlay { - if filepath.Ext(filename) != ".go" { - continue - } - modifications = append(modifications, source.FileModification{ - URI: span.URIFromPath(filename), - Action: source.Open, - Version: -1, - Text: content, - LanguageID: "go", - }) - } - if err := session.ModifyFiles(ctx, modifications); err != nil { - t.Fatal(err) - } - r := &runner{ - data: datum, - ctx: ctx, - editRecv: make(chan map[span.URI][]byte, 1), - } - - r.server = NewServer(session, testClient{runner: r}, options) - tests.Run(t, r, datum) -} - -// runner implements tests.Tests by making LSP RPCs to a gopls server. -type runner struct { - server *Server - data *tests.Data - diagnostics map[span.URI][]*source.Diagnostic - ctx context.Context - editRecv chan map[span.URI][]byte -} - -// testClient stubs any client functions that may be called by LSP functions. -type testClient struct { - protocol.Client - runner *runner -} - -func (c testClient) Close() error { - return nil -} - -// Trivially implement PublishDiagnostics so that we can call -// server.publishReports below to de-dup sent diagnostics. -func (c testClient) PublishDiagnostics(context.Context, *protocol.PublishDiagnosticsParams) error { - return nil -} - -func (c testClient) ShowMessage(context.Context, *protocol.ShowMessageParams) error { - return nil -} - -func (c testClient) ApplyEdit(ctx context.Context, params *protocol.ApplyWorkspaceEditParams) (*protocol.ApplyWorkspaceEditResult, error) { - res, err := applyTextDocumentEdits(c.runner, params.Edit.DocumentChanges) - if err != nil { - return nil, err - } - c.runner.editRecv <- res - return &protocol.ApplyWorkspaceEditResult{Applied: true}, nil -} - -func (r *runner) CallHierarchy(t *testing.T, spn span.Span, expectedCalls *tests.CallHierarchyResult) { - mapper, err := r.data.Mapper(spn.URI()) - if err != nil { - t.Fatal(err) - } - loc, err := mapper.SpanLocation(spn) - if err != nil { - t.Fatalf("failed for %v: %v", spn, err) - } - - params := &protocol.CallHierarchyPrepareParams{ - TextDocumentPositionParams: protocol.LocationTextDocumentPositionParams(loc), - } - - items, err := r.server.PrepareCallHierarchy(r.ctx, params) - if err != nil { - t.Fatal(err) - } - if len(items) == 0 { - t.Fatalf("expected call hierarchy item to be returned for identifier at %v\n", loc.Range) - } - - callLocation := protocol.Location{ - URI: items[0].URI, - Range: items[0].Range, - } - if callLocation != loc { - t.Fatalf("expected server.PrepareCallHierarchy to return identifier at %v but got %v\n", loc, callLocation) - } - - incomingCalls, err := r.server.IncomingCalls(r.ctx, &protocol.CallHierarchyIncomingCallsParams{Item: items[0]}) - if err != nil { - t.Error(err) - } - var incomingCallItems []protocol.CallHierarchyItem - for _, item := range incomingCalls { - incomingCallItems = append(incomingCallItems, item.From) - } - msg := tests.DiffCallHierarchyItems(incomingCallItems, expectedCalls.IncomingCalls) - if msg != "" { - t.Errorf("incoming calls: %s", msg) - } - - outgoingCalls, err := r.server.OutgoingCalls(r.ctx, &protocol.CallHierarchyOutgoingCallsParams{Item: items[0]}) - if err != nil { - t.Error(err) - } - var outgoingCallItems []protocol.CallHierarchyItem - for _, item := range outgoingCalls { - outgoingCallItems = append(outgoingCallItems, item.To) - } - msg = tests.DiffCallHierarchyItems(outgoingCallItems, expectedCalls.OutgoingCalls) - if msg != "" { - t.Errorf("outgoing calls: %s", msg) - } -} - -func applyTextDocumentEdits(r *runner, edits []protocol.DocumentChanges) (map[span.URI][]byte, error) { - res := make(map[span.URI][]byte) - for _, docEdits := range edits { - if docEdits.TextDocumentEdit != nil { - uri := docEdits.TextDocumentEdit.TextDocument.URI.SpanURI() - var m *protocol.Mapper - // If we have already edited this file, we use the edited version (rather than the - // file in its original state) so that we preserve our initial changes. - if content, ok := res[uri]; ok { - m = protocol.NewMapper(uri, content) - } else { - var err error - if m, err = r.data.Mapper(uri); err != nil { - return nil, err - } - } - patched, _, err := source.ApplyProtocolEdits(m, docEdits.TextDocumentEdit.Edits) - if err != nil { - return nil, err - } - res[uri] = patched - } - } - return res, nil -} diff --git a/gopls/internal/lsp/regtest/marker.go b/gopls/internal/lsp/regtest/marker.go index 215d5713ab5..f0a1f80296b 100644 --- a/gopls/internal/lsp/regtest/marker.go +++ b/gopls/internal/lsp/regtest/marker.go @@ -223,6 +223,10 @@ var update = flag.Bool("update", false, "if set, update test data during marker // textDocument/implementation query at the src location and // checks that the resulting set of locations matches want. // +// - incomingcalls(src location, want ...location): makes a +// callHierarchy/incomingCalls query at the src location, and checks that +// the set of call.From locations matches want. +// // - item(label, details, kind): defines a completion item with the provided // fields. This information is not positional, and therefore @item markers // may occur anywhere in the source. Used in conjunction with @complete, @@ -234,6 +238,10 @@ var update = flag.Bool("update", false, "if set, update test data during marker // - loc(name, location): specifies the name for a location in the source. These // locations may be referenced by other markers. // +// - outgoingcalls(src location, want ...location): makes a +// callHierarchy/outgoingCalls query at the src location, and checks that +// the set of call.To locations matches want. +// // - preparerename(src, spn, placeholder): asserts that a textDocument/prepareRename // request at the src location expands to the spn location, with given // placeholder. If placeholder is "", this is treated as a negative @@ -564,6 +572,11 @@ type marker struct { note *expect.Note } +// ctx returns the mark context. +func (m marker) ctx() context.Context { + return m.run.env.Ctx +} + // server returns the LSP server for the marker test run. func (m marker) server() protocol.Server { return m.run.env.Editor.Server @@ -730,7 +743,9 @@ var actionMarkerFuncs = map[string]func(marker){ "highlight": actionMarkerFunc(highlightMarker), "hover": actionMarkerFunc(hoverMarker), "implementation": actionMarkerFunc(implementationMarker), + "incomingcalls": actionMarkerFunc(incomingCallsMarker), "inlayhints": actionMarkerFunc(inlayhintsMarker), + "outgoingcalls": actionMarkerFunc(outgoingCallsMarker), "preparerename": actionMarkerFunc(prepareRenameMarker), "rank": actionMarkerFunc(rankMarker), "rankl": actionMarkerFunc(ranklMarker), @@ -1743,7 +1758,7 @@ func foldingRangeMarker(mark marker, g *Golden) { // formatMarker implements the @format marker. func formatMarker(mark marker, golden *Golden) { - edits, err := mark.server().Formatting(mark.run.env.Ctx, &protocol.DocumentFormattingParams{ + edits, err := mark.server().Formatting(mark.ctx(), &protocol.DocumentFormattingParams{ TextDocument: mark.document(), }) var got []byte @@ -1868,8 +1883,7 @@ func renameErrMarker(mark marker, loc protocol.Location, newName string, wantErr } func selectionRangeMarker(mark marker, loc protocol.Location, g *Golden) { - ctx := mark.run.env.Ctx - ranges, err := mark.run.env.Editor.Server.SelectionRange(ctx, &protocol.SelectionRangeParams{ + ranges, err := mark.run.env.Editor.Server.SelectionRange(mark.ctx(), &protocol.SelectionRangeParams{ TextDocument: mark.document(), Positions: []protocol.Position{loc.Range.Start}, }) @@ -2251,7 +2265,7 @@ func codeActionChanges(env *Env, uri protocol.DocumentURI, rng protocol.Range, a // refsMarker implements the @refs marker. func refsMarker(mark marker, src protocol.Location, want ...protocol.Location) { refs := func(includeDeclaration bool, want []protocol.Location) error { - got, err := mark.server().References(mark.run.env.Ctx, &protocol.ReferenceParams{ + got, err := mark.server().References(mark.ctx(), &protocol.ReferenceParams{ TextDocumentPositionParams: protocol.LocationTextDocumentPositionParams(src), Context: protocol.ReferenceContext{ IncludeDeclaration: includeDeclaration, @@ -2281,7 +2295,7 @@ func refsMarker(mark marker, src protocol.Location, want ...protocol.Location) { // implementationMarker implements the @implementation marker. func implementationMarker(mark marker, src protocol.Location, want ...protocol.Location) { - got, err := mark.server().Implementation(mark.run.env.Ctx, &protocol.ImplementationParams{ + got, err := mark.server().Implementation(mark.ctx(), &protocol.ImplementationParams{ TextDocumentPositionParams: protocol.LocationTextDocumentPositionParams(src), }) if err != nil { @@ -2293,6 +2307,82 @@ func implementationMarker(mark marker, src protocol.Location, want ...protocol.L } } +func itemLocation(item protocol.CallHierarchyItem) protocol.Location { + return protocol.Location{ + URI: item.URI, + Range: item.Range, + } +} + +func incomingCallsMarker(mark marker, src protocol.Location, want ...protocol.Location) { + getCalls := func(item protocol.CallHierarchyItem) ([]protocol.Location, error) { + calls, err := mark.server().IncomingCalls(mark.ctx(), &protocol.CallHierarchyIncomingCallsParams{Item: item}) + if err != nil { + return nil, err + } + var locs []protocol.Location + for _, call := range calls { + locs = append(locs, itemLocation(call.From)) + } + return locs, nil + } + callHierarchy(mark, src, getCalls, want) +} + +func outgoingCallsMarker(mark marker, src protocol.Location, want ...protocol.Location) { + getCalls := func(item protocol.CallHierarchyItem) ([]protocol.Location, error) { + calls, err := mark.server().OutgoingCalls(mark.ctx(), &protocol.CallHierarchyOutgoingCallsParams{Item: item}) + if err != nil { + return nil, err + } + var locs []protocol.Location + for _, call := range calls { + locs = append(locs, itemLocation(call.To)) + } + return locs, nil + } + callHierarchy(mark, src, getCalls, want) +} + +type callHierarchyFunc = func(protocol.CallHierarchyItem) ([]protocol.Location, error) + +func callHierarchy(mark marker, src protocol.Location, getCalls callHierarchyFunc, want []protocol.Location) { + items, err := mark.server().PrepareCallHierarchy(mark.ctx(), &protocol.CallHierarchyPrepareParams{ + TextDocumentPositionParams: protocol.LocationTextDocumentPositionParams(src), + }) + if err != nil { + mark.errorf("PrepareCallHierarchy failed: %v", err) + return + } + if nitems := len(items); nitems != 1 { + mark.errorf("PrepareCallHierarchy returned %d items, want exactly 1", nitems) + return + } + if loc := itemLocation(items[0]); loc != src { + mark.errorf("PrepareCallHierarchy found call %v, want %v", loc, src) + return + } + calls, err := getCalls(items[0]) + if err != nil { + mark.errorf("call hierarchy failed: %v", err) + return + } + if calls == nil { + calls = []protocol.Location{} + } + // TODO(rfindley): why aren't call hierarchy results stable? + sortLocs := func(locs []protocol.Location) { + sort.Slice(locs, func(i, j int) bool { + return protocol.CompareLocation(locs[i], locs[j]) < 0 + }) + } + sortLocs(want) + sortLocs(calls) + if d := cmp.Diff(want, calls); d != "" { + mark.errorf("call hierarchy: unexpected results (-want +got):\n%s", d) + } +} + func inlayhintsMarker(mark marker, g *Golden) { hints := mark.run.env.InlayHints(mark.path()) @@ -2326,7 +2416,7 @@ func prepareRenameMarker(mark marker, src, spn protocol.Location, placeholder st params := &protocol.PrepareRenameParams{ TextDocumentPositionParams: protocol.LocationTextDocumentPositionParams(src), } - got, err := mark.run.env.Editor.Server.PrepareRename(mark.run.env.Ctx, params) + got, err := mark.run.env.Editor.Server.PrepareRename(mark.ctx(), params) if err != nil { mark.run.env.T.Fatal(err) } @@ -2345,7 +2435,7 @@ func prepareRenameMarker(mark marker, src, spn protocol.Location, placeholder st // symbolMarker implements the @symbol marker. func symbolMarker(mark marker, golden *Golden) { // Retrieve information about all symbols in this file. - symbols, err := mark.server().DocumentSymbol(mark.run.env.Ctx, &protocol.DocumentSymbolParams{ + symbols, err := mark.server().DocumentSymbol(mark.ctx(), &protocol.DocumentSymbolParams{ TextDocument: protocol.TextDocumentIdentifier{URI: mark.uri()}, }) if err != nil { @@ -2434,7 +2524,7 @@ func workspaceSymbolMarker(mark marker, query string, golden *Golden) { Query: query, } - gotSymbols, err := mark.server().Symbol(mark.run.env.Ctx, params) + gotSymbols, err := mark.server().Symbol(mark.ctx(), params) if err != nil { mark.errorf("Symbol(%q) failed: %v", query, err) return diff --git a/gopls/internal/lsp/reset_golden.sh b/gopls/internal/lsp/reset_golden.sh deleted file mode 100755 index ff7f4d08208..00000000000 --- a/gopls/internal/lsp/reset_golden.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/bin/bash -# -# Copyright 2022 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. -# -# Updates the *.golden files ... to match the tests' current behavior. - -set -eu - -GO117BIN="go1.17.9" - -command -v $GO117BIN >/dev/null 2>&1 || { - go install golang.org/dl/$GO117BIN@latest - $GO117BIN download -} - -find ./internal/lsp/testdata -name *.golden ! -name summary*.txt.golden -delete -# Here we intentionally do not run the ./internal/lsp/source tests with -# -golden. Eventually these tests will be deleted, and in the meantime they are -# redundant with the ./internal/lsp tests. -# -# Note: go1.17.9 tests must be run *before* go tests, as by convention the -# golden output should match the output of gopls built with the most recent -# version of Go. If output differs at 1.17, tests must be tolerant of the 1.17 -# output. -$GO117BIN test ./internal/lsp -golden -go test ./internal/lsp -golden -$GO117BIN test ./test -golden -go test ./test -golden diff --git a/gopls/internal/lsp/testdata/callhierarchy/callhierarchy.go b/gopls/internal/lsp/testdata/callhierarchy/callhierarchy.go deleted file mode 100644 index 252e8054f40..00000000000 --- a/gopls/internal/lsp/testdata/callhierarchy/callhierarchy.go +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright 2020 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 callhierarchy - -import "golang.org/lsptests/callhierarchy/outgoing" - -func a() { //@mark(hierarchyA, "a") - D() -} - -func b() { //@mark(hierarchyB, "b") - D() -} - -// C is an exported function -func C() { //@mark(hierarchyC, "C") - D() - D() -} - -// To test hierarchy across function literals -var x = func() { //@mark(hierarchyLiteral, "func"),mark(hierarchyLiteralOut, "x") - D() -} - -// D is exported to test incoming/outgoing calls across packages -func D() { //@mark(hierarchyD, "D"),incomingcalls(hierarchyD, hierarchyA, hierarchyB, hierarchyC, hierarchyLiteral, incomingA),outgoingcalls(hierarchyD, hierarchyE, hierarchyF, hierarchyG, hierarchyLiteralOut, outgoingB, hierarchyFoo, hierarchyH, hierarchyI, hierarchyJ, hierarchyK) - e() - x() - F() - outgoing.B() - foo := func() {} //@mark(hierarchyFoo, "foo"),incomingcalls(hierarchyFoo, hierarchyD),outgoingcalls(hierarchyFoo) - foo() - - func() { - g() - }() - - var i Interface = impl{} - i.H() - i.I() - - s := Struct{} - s.J() - s.K() -} - -func e() {} //@mark(hierarchyE, "e") - -// F is an exported function -func F() {} //@mark(hierarchyF, "F") - -func g() {} //@mark(hierarchyG, "g") - -type Interface interface { - H() //@mark(hierarchyH, "H") - I() //@mark(hierarchyI, "I") -} - -type impl struct{} - -func (i impl) H() {} -func (i impl) I() {} - -type Struct struct { - J func() //@mark(hierarchyJ, "J") - K func() //@mark(hierarchyK, "K") -} diff --git a/gopls/internal/lsp/testdata/callhierarchy/incoming/incoming.go b/gopls/internal/lsp/testdata/callhierarchy/incoming/incoming.go deleted file mode 100644 index c629aa87929..00000000000 --- a/gopls/internal/lsp/testdata/callhierarchy/incoming/incoming.go +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright 2020 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 incoming - -import "golang.org/lsptests/callhierarchy" - -// A is exported to test incoming calls across packages -func A() { //@mark(incomingA, "A") - callhierarchy.D() -} diff --git a/gopls/internal/lsp/testdata/callhierarchy/outgoing/outgoing.go b/gopls/internal/lsp/testdata/callhierarchy/outgoing/outgoing.go deleted file mode 100644 index 74362d419c3..00000000000 --- a/gopls/internal/lsp/testdata/callhierarchy/outgoing/outgoing.go +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright 2020 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 outgoing - -// B is exported to test outgoing calls across packages -func B() { //@mark(outgoingB, "B") -} diff --git a/gopls/internal/lsp/testdata/summary.txt.golden b/gopls/internal/lsp/testdata/summary.txt.golden deleted file mode 100644 index b4e7101c0ef..00000000000 --- a/gopls/internal/lsp/testdata/summary.txt.golden +++ /dev/null @@ -1,3 +0,0 @@ --- summary -- -CallHierarchyCount = 2 - diff --git a/gopls/internal/lsp/tests/README.md b/gopls/internal/lsp/tests/README.md deleted file mode 100644 index 07df28815c1..00000000000 --- a/gopls/internal/lsp/tests/README.md +++ /dev/null @@ -1,66 +0,0 @@ -# Testing - -LSP has "marker tests" defined in `internal/lsp/testdata`, as well as -traditional tests. - -## Marker tests - -Marker tests have a standard input file, like -`internal/lsp/testdata/foo/bar.go`, and some may have a corresponding golden -file, like `internal/lsp/testdata/foo/bar.go.golden`. The former is the "input" -and the latter is the expected output. - -Each input file contains annotations like -`//@suggestedfix("}", "refactor.rewrite", "Fill anonymous struct")`. These annotations are interpreted by -test runners to perform certain actions. The expected output after those actions -is encoded in the golden file. - -When tests are run, each annotation results in a new subtest, which is encoded -in the golden file with a heading like, - -```bash --- suggestedfix_bar_11_21 -- -// expected contents go here --- suggestedfix_bar_13_20 -- -// expected contents go here -``` - -The format of these headings vary: they are defined by the -[`Golden`](https://pkg.go.dev/golang.org/x/tools/gopls/internal/lsp/tests#Data.Golden) -function for each annotation. In the case above, the format is: annotation -name, file name, annotation line location, annotation character location. - -So, if `internal/lsp/testdata/foo/bar.go` has three `suggestedfix` annotations, -the golden file should have three headers with `suggestedfix_bar_xx_yy` -headings. - -To see a list of all available annotations, see the exported "expectations" in -[tests.go](https://github.com/golang/tools/blob/299f270db45902e93469b1152fafed034bb3f033/internal/lsp/tests/tests.go#L418-L447). - -To run marker tests, - -```bash -cd /path/to/tools - -# The marker tests are located in "internal/lsp", "internal/lsp/cmd, and -# "internal/lsp/source". -go test ./internal/lsp/... -``` - -There are quite a lot of marker tests, so to run one individually, pass the test -path and heading into a -run argument: - -```bash -cd /path/to/tools -go test ./internal/lsp/... -v -run TestLSP/Modules/SuggestedFix/bar_11_21 -``` - -## Resetting marker tests - -Sometimes, a change is made to lsp that requires a change to multiple golden -files. When this happens, you can run, - -```bash -cd /path/to/tools -./internal/lsp/reset_golden.sh -``` diff --git a/gopls/internal/lsp/tests/tests.go b/gopls/internal/lsp/tests/tests.go deleted file mode 100644 index bb1695456d0..00000000000 --- a/gopls/internal/lsp/tests/tests.go +++ /dev/null @@ -1,512 +0,0 @@ -// 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" - "os" - "path/filepath" - "sort" - "strings" - "sync" - "testing" - "time" - - "golang.org/x/tools/go/packages" - "golang.org/x/tools/go/packages/packagestest" - "golang.org/x/tools/gopls/internal/lsp/protocol" - "golang.org/x/tools/gopls/internal/lsp/source" - "golang.org/x/tools/gopls/internal/lsp/tests/compare" - "golang.org/x/tools/gopls/internal/span" - "golang.org/x/tools/internal/typeparams" - "golang.org/x/tools/txtar" -) - -const ( - overlayFileSuffix = ".overlay" - goldenFileSuffix = ".golden" - inFileSuffix = ".in" - summaryFile = "summary.txt" - - // The module path containing the testdata packages. - // - // Warning: the length of this module path matters, as we have bumped up - // against command-line limitations on windows (golang/go#54800). - testModule = "golang.org/lsptests" -) - -var UpdateGolden = flag.Bool("golden", false, "Update golden files") - -// These type names apparently avoid the need to repeat the -// type in the field name and the make() expression. -type CallHierarchy = map[span.Span]*CallHierarchyResult - -type Data struct { - Config packages.Config - Exported *packagestest.Exported - CallHierarchy CallHierarchy - - fragments map[string]string - dir string - golden map[string]*Golden - mode string - - ModfileFlagAvailable bool - - mappersMu sync.Mutex - mappers map[span.URI]*protocol.Mapper -} - -// The Tests interface abstracts the LSP-based implementation of the marker -// test operators appearing in files beneath ../testdata/. -// -// TODO(adonovan): reduce duplication; see https://github.com/golang/go/issues/54845. -// There is only one implementation (*runner in ../lsp_test.go), so -// we can abolish the interface now. -type Tests interface { - CallHierarchy(*testing.T, span.Span, *CallHierarchyResult) -} - -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 SuggestedFix struct { - ActionKind, Title string -} - -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.RefactorInline: true, - protocol.RefactorExtract: true, - protocol.SourceFixAll: true, - }, - source.Mod: { - protocol.SourceOrganizeImports: true, - }, - source.Sum: {}, - source.Work: {}, - source.Tmpl: {}, - } - o.InsertTextFormat = protocol.SnippetTextFormat - o.CompletionBudget = time.Minute - o.HierarchicalDocumentSymbolSupport = true - o.SemanticTokens = true - o.InternalOptions.NewDiff = "new" - - // Enable all inlay hints. - if o.Hints == nil { - o.Hints = make(map[string]bool) - } - for name := range source.AllInlayHints { - o.Hints[name] = 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) { - datum := load(t, mode, dataDir) - t.Helper() - f(t, datum) - }) - } -} - -func load(t testing.TB, mode string, dir string) *Data { - datum := &Data{ - CallHierarchy: make(CallHierarchy), - - dir: dir, - fragments: map[string]string{}, - golden: map[string]*Golden{}, - mode: mode, - mappers: map[span.URI]*protocol.Mapper{}, - } - - 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) - // Prune test cases that exercise generics. - if !typeparams.Enabled { - for name := range files { - if strings.Contains(name, "_generics") { - delete(files, name) - } - } - } - 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 := os.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{}{ - "incomingcalls": datum.collectIncomingCalls, - "outgoingcalls": datum.collectOutgoingCalls, - }); err != nil { - t.Fatal(err) - } - - if mode == "MultiModule" { - if err := moveFile(filepath.Join(datum.Config.Dir, "go.mod"), filepath.Join(datum.Config.Dir, "testmodule/go.mod")); err != nil { - t.Fatal(err) - } - } - - return datum -} - -// moveFile moves the file at oldpath to newpath, by renaming if possible -// or copying otherwise. -func moveFile(oldpath, newpath string) (err error) { - renameErr := os.Rename(oldpath, newpath) - if renameErr == nil { - return nil - } - - src, err := os.Open(oldpath) - if err != nil { - return err - } - defer func() { - src.Close() - if err == nil { - err = os.Remove(oldpath) - } - }() - - perm := os.ModePerm - fi, err := src.Stat() - if err == nil { - perm = fi.Mode().Perm() - } - - dst, err := os.OpenFile(newpath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, perm) - if err != nil { - return err - } - - _, err = io.Copy(dst, src) - if closeErr := dst.Close(); err == nil { - err = closeErr - } - return err -} - -func Run(t *testing.T, tests Tests, data *Data) { - t.Helper() - checkData(t, data) - - 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) - }) - } - }) - - 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 := os.WriteFile(golden.Filename, txtar.Format(golden.Archive), 0666); err != nil { - t.Fatal(err) - } - } - } -} - -func checkData(t *testing.T, data *Data) { - buf := &bytes.Buffer{} - - fmt.Fprintf(buf, "CallHierarchyCount = %v\n", len(data.CallHierarchy)) - - want := string(data.Golden(t, "summary", summaryFile, func() ([]byte, error) { - return buf.Bytes(), nil - })) - got := buf.String() - if want != got { - // These counters change when assertions are added or removed. - // They act as an independent safety net to ensure that the - // tests didn't spuriously pass because they did no work. - t.Errorf("test summary does not match:\n%s\n(Run with -golden to update golden file; also, there may be one per Go version.)", compare.Text(want, got)) - } -} - -func (data *Data) Mapper(uri span.URI) (*protocol.Mapper, 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 - } - data.mappers[uri] = protocol.NewMapper(uri, content) - } - return data.mappers[uri], nil -} - -func (data *Data) Golden(t *testing.T, tag, target string, update func() ([]byte, error)) []byte { - t.Helper() - fragment, found := data.fragments[target] - if !found { - if filepath.IsAbs(target) { - t.Fatalf("invalid golden file fragment %v", target) - } - fragment = target - } - golden := data.golden[fragment] - if golden == nil { - if !*UpdateGolden { - 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 { - 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 { - 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) collectIncomingCalls(src span.Span, calls []span.Span) { - for _, call := range calls { - rng := data.mustRange(call) - // 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) { - if data.CallHierarchy[src] == nil { - data.CallHierarchy[src] = &CallHierarchyResult{} - } - for _, call := range calls { - // we're only comparing protocol.range - data.CallHierarchy[src].OutgoingCalls = append(data.CallHierarchy[src].OutgoingCalls, - protocol.CallHierarchyItem{ - URI: protocol.DocumentURI(call.URI()), - Range: data.mustRange(call), - }) - } -} - -// mustRange converts spn into a protocol.Range, panicking on any error. -func (data *Data) mustRange(spn span.Span) protocol.Range { - m, err := data.Mapper(spn.URI()) - rng, err := m.SpanRange(spn) - if err != nil { - panic(fmt.Sprintf("converting span %s to range: %v", spn, err)) - } - return rng -} - -func uriName(uri span.URI) string { - return filepath.Base(strings.TrimSuffix(uri.Filename(), ".go")) -} - -// TODO(golang/go#54845): improve the formatting here to match standard -// line:column position formatting. -func SpanName(spn span.Span) string { - return fmt.Sprintf("%v_%v_%v", uriName(spn.URI()), spn.Start().Line(), spn.Start().Column()) -} diff --git a/gopls/internal/lsp/tests/util.go b/gopls/internal/lsp/tests/util.go deleted file mode 100644 index ea0920d2e62..00000000000 --- a/gopls/internal/lsp/tests/util.go +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright 2020 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 ( - "fmt" - - "golang.org/x/tools/gopls/internal/lsp/protocol" -) - -// DiffCallHierarchyItems returns the diff between expected and actual call locations for incoming/outgoing call hierarchies -func DiffCallHierarchyItems(gotCalls []protocol.CallHierarchyItem, expectedCalls []protocol.CallHierarchyItem) string { - expected := make(map[protocol.Location]bool) - for _, call := range expectedCalls { - expected[protocol.Location{URI: call.URI, Range: call.Range}] = true - } - - got := make(map[protocol.Location]bool) - for _, call := range gotCalls { - got[protocol.Location{URI: call.URI, Range: call.Range}] = true - } - if len(got) != len(expected) { - return fmt.Sprintf("expected %d calls but got %d", len(expected), len(got)) - } - for spn := range got { - if !expected[spn] { - return fmt.Sprintf("incorrect calls, expected locations %v but got locations %v", expected, got) - } - } - return "" -} diff --git a/gopls/internal/regtest/marker/testdata/callhierarchy/callhierarchy.txt b/gopls/internal/regtest/marker/testdata/callhierarchy/callhierarchy.txt new file mode 100644 index 00000000000..2621f6709fc --- /dev/null +++ b/gopls/internal/regtest/marker/testdata/callhierarchy/callhierarchy.txt @@ -0,0 +1,94 @@ +This test checks call hierarchy queries. + +-ignore_extra_diags due to the initialization cycle. + +-- flags -- +-ignore_extra_diags + +-- go.mod -- +module golang.org/lsptests/callhierarchy + +-- incoming/incoming.go -- +package incoming + +import "golang.org/lsptests/callhierarchy" + +// A is exported to test incoming calls across packages +func A() { //@loc(incomingA, "A") + callhierarchy.D() +} + +-- outgoing/outgoing.go -- +package outgoing + +// B is exported to test outgoing calls across packages +func B() { //@loc(outgoingB, "B") +} + +-- hierarchy.go -- +package callhierarchy + +import "golang.org/lsptests/callhierarchy/outgoing" + +func a() { //@loc(hierarchyA, "a") + D() +} + +func b() { //@loc(hierarchyB, "b") + D() +} + +// C is an exported function +func C() { //@loc(hierarchyC, "C") + D() + D() +} + +// To test hierarchy across function literals +var x = func() { //@loc(hierarchyLiteral, "func"),loc(hierarchyLiteralOut, "x") + D() +} + +// D is exported to test incoming/outgoing calls across packages +func D() { //@loc(hierarchyD, "D"),incomingcalls(hierarchyD, hierarchyA, hierarchyB, hierarchyC, hierarchyLiteral, incomingA),outgoingcalls(hierarchyD, hierarchyE, hierarchyF, hierarchyG, hierarchyLiteralOut, outgoingB, hierarchyFoo, hierarchyH, hierarchyI, hierarchyJ, hierarchyK) + e() + x() + F() + outgoing.B() + foo := func() {} //@loc(hierarchyFoo, "foo"),incomingcalls(hierarchyFoo, hierarchyD),outgoingcalls(hierarchyFoo) + foo() + + func() { + g() + }() + + var i Interface = impl{} + i.H() + i.I() + + s := Struct{} + s.J() + s.K() +} + +func e() {} //@loc(hierarchyE, "e") + +// F is an exported function +func F() {} //@loc(hierarchyF, "F") + +func g() {} //@loc(hierarchyG, "g") + +type Interface interface { + H() //@loc(hierarchyH, "H") + I() //@loc(hierarchyI, "I") +} + +type impl struct{} + +func (i impl) H() {} +func (i impl) I() {} + +type Struct struct { + J func() //@loc(hierarchyJ, "J") + K func() //@loc(hierarchyK, "K") +}