golang.org/x/tools/gopls@v0.15.3/internal/cmd/capabilities_test.go (about) 1 // Copyright 2019 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package cmd 6 7 import ( 8 "context" 9 "fmt" 10 "os" 11 "path/filepath" 12 "testing" 13 14 "golang.org/x/tools/gopls/internal/cache" 15 "golang.org/x/tools/gopls/internal/protocol" 16 "golang.org/x/tools/gopls/internal/server" 17 "golang.org/x/tools/gopls/internal/settings" 18 "golang.org/x/tools/internal/testenv" 19 ) 20 21 // TestCapabilities does some minimal validation of the server's adherence to the LSP. 22 // The checks in the test are added as changes are made and errors noticed. 23 func TestCapabilities(t *testing.T) { 24 // TODO(bcmills): This test fails on js/wasm, which is not unexpected, but the 25 // failure mode is that the DidOpen call below reports "no views in session", 26 // which seems a little too cryptic. 27 // Is there some missing error reporting somewhere? 28 testenv.NeedsTool(t, "go") 29 30 tmpDir, err := os.MkdirTemp("", "fake") 31 if err != nil { 32 t.Fatal(err) 33 } 34 tmpFile := filepath.Join(tmpDir, "fake.go") 35 if err := os.WriteFile(tmpFile, []byte(""), 0775); err != nil { 36 t.Fatal(err) 37 } 38 if err := os.WriteFile(filepath.Join(tmpDir, "go.mod"), []byte("module fake\n\ngo 1.12\n"), 0775); err != nil { 39 t.Fatal(err) 40 } 41 defer os.RemoveAll(tmpDir) 42 43 app := New(nil) 44 45 params := &protocol.ParamInitialize{} 46 params.RootURI = protocol.URIFromPath(tmpDir) 47 params.Capabilities.Workspace.Configuration = true 48 49 // Send an initialize request to the server. 50 ctx := context.Background() 51 client := newClient(app, nil) 52 options := settings.DefaultOptions(app.options) 53 server := server.New(cache.NewSession(ctx, cache.New(nil)), client, options) 54 result, err := server.Initialize(ctx, params) 55 if err != nil { 56 t.Fatal(err) 57 } 58 // Validate initialization result. 59 if err := validateCapabilities(result); err != nil { 60 t.Error(err) 61 } 62 // Complete initialization of server. 63 if err := server.Initialized(ctx, &protocol.InitializedParams{}); err != nil { 64 t.Fatal(err) 65 } 66 67 c := newConnection(server, client) 68 defer c.terminate(ctx) 69 70 // Open the file on the server side. 71 uri := protocol.URIFromPath(tmpFile) 72 if err := c.Server.DidOpen(ctx, &protocol.DidOpenTextDocumentParams{ 73 TextDocument: protocol.TextDocumentItem{ 74 URI: uri, 75 LanguageID: "go", 76 Version: 1, 77 Text: `package main; func main() {};`, 78 }, 79 }); err != nil { 80 t.Fatal(err) 81 } 82 83 // If we are sending a full text change, the change.Range must be nil. 84 // It is not enough for the Change to be empty, as that is ambiguous. 85 if err := c.Server.DidChange(ctx, &protocol.DidChangeTextDocumentParams{ 86 TextDocument: protocol.VersionedTextDocumentIdentifier{ 87 TextDocumentIdentifier: protocol.TextDocumentIdentifier{ 88 URI: uri, 89 }, 90 Version: 2, 91 }, 92 ContentChanges: []protocol.TextDocumentContentChangeEvent{ 93 { 94 Range: nil, 95 Text: `package main; func main() { fmt.Println("") }`, 96 }, 97 }, 98 }); err != nil { 99 t.Fatal(err) 100 } 101 102 // Send a code action request to validate expected types. 103 actions, err := c.Server.CodeAction(ctx, &protocol.CodeActionParams{ 104 TextDocument: protocol.TextDocumentIdentifier{ 105 URI: uri, 106 }, 107 }) 108 if err != nil { 109 t.Fatal(err) 110 } 111 for _, action := range actions { 112 // Validate that an empty command is sent along with import organization responses. 113 if action.Kind == protocol.SourceOrganizeImports && action.Command != nil { 114 t.Errorf("unexpected command for import organization") 115 } 116 } 117 118 if err := c.Server.DidSave(ctx, &protocol.DidSaveTextDocumentParams{ 119 TextDocument: protocol.TextDocumentIdentifier{ 120 URI: uri, 121 }, 122 // LSP specifies that a file can be saved with optional text, so this field must be nil. 123 Text: nil, 124 }); err != nil { 125 t.Fatal(err) 126 } 127 128 // Send a completion request to validate expected types. 129 list, err := c.Server.Completion(ctx, &protocol.CompletionParams{ 130 TextDocumentPositionParams: protocol.TextDocumentPositionParams{ 131 TextDocument: protocol.TextDocumentIdentifier{ 132 URI: uri, 133 }, 134 Position: protocol.Position{ 135 Line: 0, 136 Character: 28, 137 }, 138 }, 139 }) 140 if err != nil { 141 t.Fatal(err) 142 } 143 for _, item := range list.Items { 144 // All other completion items should have nil commands. 145 // An empty command will be treated as a command with the name '' by VS Code. 146 // This causes VS Code to report errors to users about invalid commands. 147 if item.Command != nil { 148 t.Errorf("unexpected command for completion item") 149 } 150 // The item's TextEdit must be a pointer, as VS Code considers TextEdits 151 // that don't contain the cursor position to be invalid. 152 var textEdit interface{} = item.TextEdit 153 if _, ok := textEdit.(*protocol.TextEdit); !ok { 154 t.Errorf("textEdit is not a *protocol.TextEdit, instead it is %T", textEdit) 155 } 156 } 157 if err := c.Server.Shutdown(ctx); err != nil { 158 t.Fatal(err) 159 } 160 if err := c.Server.Exit(ctx); err != nil { 161 t.Fatal(err) 162 } 163 } 164 165 func validateCapabilities(result *protocol.InitializeResult) error { 166 // If the client sends "false" for RenameProvider.PrepareSupport, 167 // the server must respond with a boolean. 168 if v, ok := result.Capabilities.RenameProvider.(bool); !ok { 169 return fmt.Errorf("RenameProvider must be a boolean if PrepareSupport is false (got %T)", v) 170 } 171 // The same goes for CodeActionKind.ValueSet. 172 if v, ok := result.Capabilities.CodeActionProvider.(bool); !ok { 173 return fmt.Errorf("CodeActionSupport must be a boolean if CodeActionKind.ValueSet has length 0 (got %T)", v) 174 } 175 return nil 176 }