github.com/jhump/golang-x-tools@v0.0.0-20220218190644-4958d6d39439/internal/lsp/regtest/wrappers.go (about) 1 // Copyright 2020 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 regtest 6 7 import ( 8 "encoding/json" 9 "path" 10 "testing" 11 12 "github.com/jhump/golang-x-tools/internal/lsp/command" 13 "github.com/jhump/golang-x-tools/internal/lsp/fake" 14 "github.com/jhump/golang-x-tools/internal/lsp/protocol" 15 ) 16 17 func (e *Env) ChangeFilesOnDisk(events []fake.FileEvent) { 18 e.T.Helper() 19 if err := e.Sandbox.Workdir.ChangeFilesOnDisk(e.Ctx, events); err != nil { 20 e.T.Fatal(err) 21 } 22 } 23 24 // RemoveWorkspaceFile deletes a file on disk but does nothing in the 25 // editor. It calls t.Fatal on any error. 26 func (e *Env) RemoveWorkspaceFile(name string) { 27 e.T.Helper() 28 if err := e.Sandbox.Workdir.RemoveFile(e.Ctx, name); err != nil { 29 e.T.Fatal(err) 30 } 31 } 32 33 // ReadWorkspaceFile reads a file from the workspace, calling t.Fatal on any 34 // error. 35 func (e *Env) ReadWorkspaceFile(name string) string { 36 e.T.Helper() 37 content, err := e.Sandbox.Workdir.ReadFile(name) 38 if err != nil { 39 e.T.Fatal(err) 40 } 41 return content 42 } 43 44 // WriteWorkspaceFile writes a file to disk but does nothing in the editor. 45 // It calls t.Fatal on any error. 46 func (e *Env) WriteWorkspaceFile(name, content string) { 47 e.T.Helper() 48 if err := e.Sandbox.Workdir.WriteFile(e.Ctx, name, content); err != nil { 49 e.T.Fatal(err) 50 } 51 } 52 53 // WriteWorkspaceFiles deletes a file on disk but does nothing in the 54 // editor. It calls t.Fatal on any error. 55 func (e *Env) WriteWorkspaceFiles(files map[string]string) { 56 e.T.Helper() 57 if err := e.Sandbox.Workdir.WriteFiles(e.Ctx, files); err != nil { 58 e.T.Fatal(err) 59 } 60 } 61 62 // OpenFile opens a file in the editor, calling t.Fatal on any error. 63 func (e *Env) OpenFile(name string) { 64 e.T.Helper() 65 if err := e.Editor.OpenFile(e.Ctx, name); err != nil { 66 e.T.Fatal(err) 67 } 68 } 69 70 // CreateBuffer creates a buffer in the editor, calling t.Fatal on any error. 71 func (e *Env) CreateBuffer(name string, content string) { 72 e.T.Helper() 73 if err := e.Editor.CreateBuffer(e.Ctx, name, content); err != nil { 74 e.T.Fatal(err) 75 } 76 } 77 78 // CloseBuffer closes an editor buffer without saving, calling t.Fatal on any 79 // error. 80 func (e *Env) CloseBuffer(name string) { 81 e.T.Helper() 82 if err := e.Editor.CloseBuffer(e.Ctx, name); err != nil { 83 e.T.Fatal(err) 84 } 85 } 86 87 // EditBuffer applies edits to an editor buffer, calling t.Fatal on any error. 88 func (e *Env) EditBuffer(name string, edits ...fake.Edit) { 89 e.T.Helper() 90 if err := e.Editor.EditBuffer(e.Ctx, name, edits); err != nil { 91 e.T.Fatal(err) 92 } 93 } 94 95 func (e *Env) SetBufferContent(name string, content string) { 96 e.T.Helper() 97 if err := e.Editor.SetBufferContent(e.Ctx, name, content); err != nil { 98 e.T.Fatal(err) 99 } 100 } 101 102 // RegexpRange returns the range of the first match for re in the buffer 103 // specified by name, calling t.Fatal on any error. It first searches for the 104 // position in open buffers, then in workspace files. 105 func (e *Env) RegexpRange(name, re string) (fake.Pos, fake.Pos) { 106 e.T.Helper() 107 start, end, err := e.Editor.RegexpRange(name, re) 108 if err == fake.ErrUnknownBuffer { 109 start, end, err = e.Sandbox.Workdir.RegexpRange(name, re) 110 } 111 if err != nil { 112 e.T.Fatalf("RegexpRange: %v, %v", name, err) 113 } 114 return start, end 115 } 116 117 // RegexpSearch returns the starting position of the first match for re in the 118 // buffer specified by name, calling t.Fatal on any error. It first searches 119 // for the position in open buffers, then in workspace files. 120 func (e *Env) RegexpSearch(name, re string) fake.Pos { 121 e.T.Helper() 122 pos, err := e.Editor.RegexpSearch(name, re) 123 if err == fake.ErrUnknownBuffer { 124 pos, err = e.Sandbox.Workdir.RegexpSearch(name, re) 125 } 126 if err != nil { 127 e.T.Fatalf("RegexpSearch: %v, %v", name, err) 128 } 129 return pos 130 } 131 132 // RegexpReplace replaces the first group in the first match of regexpStr with 133 // the replace text, calling t.Fatal on any error. 134 func (e *Env) RegexpReplace(name, regexpStr, replace string) { 135 e.T.Helper() 136 if err := e.Editor.RegexpReplace(e.Ctx, name, regexpStr, replace); err != nil { 137 e.T.Fatalf("RegexpReplace: %v", err) 138 } 139 } 140 141 // SaveBuffer saves an editor buffer, calling t.Fatal on any error. 142 func (e *Env) SaveBuffer(name string) { 143 e.T.Helper() 144 if err := e.Editor.SaveBuffer(e.Ctx, name); err != nil { 145 e.T.Fatal(err) 146 } 147 } 148 149 func (e *Env) SaveBufferWithoutActions(name string) { 150 e.T.Helper() 151 if err := e.Editor.SaveBufferWithoutActions(e.Ctx, name); err != nil { 152 e.T.Fatal(err) 153 } 154 } 155 156 // GoToDefinition goes to definition in the editor, calling t.Fatal on any 157 // error. It returns the path and position of the resulting jump. 158 func (e *Env) GoToDefinition(name string, pos fake.Pos) (string, fake.Pos) { 159 e.T.Helper() 160 n, p, err := e.Editor.GoToDefinition(e.Ctx, name, pos) 161 if err != nil { 162 e.T.Fatal(err) 163 } 164 return n, p 165 } 166 167 // Symbol returns symbols matching query 168 func (e *Env) Symbol(query string) []fake.SymbolInformation { 169 e.T.Helper() 170 r, err := e.Editor.Symbol(e.Ctx, query) 171 if err != nil { 172 e.T.Fatal(err) 173 } 174 return r 175 } 176 177 // FormatBuffer formats the editor buffer, calling t.Fatal on any error. 178 func (e *Env) FormatBuffer(name string) { 179 e.T.Helper() 180 if err := e.Editor.FormatBuffer(e.Ctx, name); err != nil { 181 e.T.Fatal(err) 182 } 183 } 184 185 // OrganizeImports processes the source.organizeImports codeAction, calling 186 // t.Fatal on any error. 187 func (e *Env) OrganizeImports(name string) { 188 e.T.Helper() 189 if err := e.Editor.OrganizeImports(e.Ctx, name); err != nil { 190 e.T.Fatal(err) 191 } 192 } 193 194 // ApplyQuickFixes processes the quickfix codeAction, calling t.Fatal on any error. 195 func (e *Env) ApplyQuickFixes(path string, diagnostics []protocol.Diagnostic) { 196 e.T.Helper() 197 if err := e.Editor.ApplyQuickFixes(e.Ctx, path, nil, diagnostics); err != nil { 198 e.T.Fatal(err) 199 } 200 } 201 202 // ApplyCodeAction applies the given code action. 203 func (e *Env) ApplyCodeAction(action protocol.CodeAction) { 204 e.T.Helper() 205 if err := e.Editor.ApplyCodeAction(e.Ctx, action); err != nil { 206 e.T.Fatal(err) 207 } 208 } 209 210 // GetQuickFixes returns the available quick fix code actions. 211 func (e *Env) GetQuickFixes(path string, diagnostics []protocol.Diagnostic) []protocol.CodeAction { 212 e.T.Helper() 213 actions, err := e.Editor.GetQuickFixes(e.Ctx, path, nil, diagnostics) 214 if err != nil { 215 e.T.Fatal(err) 216 } 217 return actions 218 } 219 220 // Hover in the editor, calling t.Fatal on any error. 221 func (e *Env) Hover(name string, pos fake.Pos) (*protocol.MarkupContent, fake.Pos) { 222 e.T.Helper() 223 c, p, err := e.Editor.Hover(e.Ctx, name, pos) 224 if err != nil { 225 e.T.Fatal(err) 226 } 227 return c, p 228 } 229 230 func (e *Env) DocumentLink(name string) []protocol.DocumentLink { 231 e.T.Helper() 232 links, err := e.Editor.DocumentLink(e.Ctx, name) 233 if err != nil { 234 e.T.Fatal(err) 235 } 236 return links 237 } 238 239 func (e *Env) DocumentHighlight(name string, pos fake.Pos) []protocol.DocumentHighlight { 240 e.T.Helper() 241 highlights, err := e.Editor.DocumentHighlight(e.Ctx, name, pos) 242 if err != nil { 243 e.T.Fatal(err) 244 } 245 return highlights 246 } 247 248 // RunGenerate runs go:generate on the given dir, calling t.Fatal on any error. 249 // It waits for the generate command to complete and checks for file changes 250 // before returning. 251 func (e *Env) RunGenerate(dir string) { 252 e.T.Helper() 253 if err := e.Editor.RunGenerate(e.Ctx, dir); err != nil { 254 e.T.Fatal(err) 255 } 256 e.Await(NoOutstandingWork()) 257 // Ideally the fake.Workspace would handle all synthetic file watching, but 258 // we help it out here as we need to wait for the generate command to 259 // complete before checking the filesystem. 260 e.CheckForFileChanges() 261 } 262 263 // RunGoCommand runs the given command in the sandbox's default working 264 // directory. 265 func (e *Env) RunGoCommand(verb string, args ...string) { 266 e.T.Helper() 267 if err := e.Sandbox.RunGoCommand(e.Ctx, "", verb, args, true); err != nil { 268 e.T.Fatal(err) 269 } 270 } 271 272 // RunGoCommandInDir is like RunGoCommand, but executes in the given 273 // relative directory of the sandbox. 274 func (e *Env) RunGoCommandInDir(dir, verb string, args ...string) { 275 e.T.Helper() 276 if err := e.Sandbox.RunGoCommand(e.Ctx, dir, verb, args, true); err != nil { 277 e.T.Fatal(err) 278 } 279 } 280 281 // DumpGoSum prints the correct go.sum contents for dir in txtar format, 282 // for use in creating regtests. 283 func (e *Env) DumpGoSum(dir string) { 284 e.T.Helper() 285 286 if err := e.Sandbox.RunGoCommand(e.Ctx, dir, "list", []string{"-mod=mod", "..."}, true); err != nil { 287 e.T.Fatal(err) 288 } 289 sumFile := path.Join(dir, "/go.sum") 290 e.T.Log("\n\n-- " + sumFile + " --\n" + e.ReadWorkspaceFile(sumFile)) 291 e.T.Fatal("see contents above") 292 } 293 294 // CheckForFileChanges triggers a manual poll of the workspace for any file 295 // changes since creation, or since last polling. It is a workaround for the 296 // lack of true file watching support in the fake workspace. 297 func (e *Env) CheckForFileChanges() { 298 e.T.Helper() 299 if err := e.Sandbox.Workdir.CheckForFileChanges(e.Ctx); err != nil { 300 e.T.Fatal(err) 301 } 302 } 303 304 // CodeLens calls textDocument/codeLens for the given path, calling t.Fatal on 305 // any error. 306 func (e *Env) CodeLens(path string) []protocol.CodeLens { 307 e.T.Helper() 308 lens, err := e.Editor.CodeLens(e.Ctx, path) 309 if err != nil { 310 e.T.Fatal(err) 311 } 312 return lens 313 } 314 315 // ExecuteCodeLensCommand executes the command for the code lens matching the 316 // given command name. 317 func (e *Env) ExecuteCodeLensCommand(path string, cmd command.Command) { 318 e.T.Helper() 319 lenses := e.CodeLens(path) 320 var lens protocol.CodeLens 321 var found bool 322 for _, l := range lenses { 323 if l.Command.Command == cmd.ID() { 324 lens = l 325 found = true 326 } 327 } 328 if !found { 329 e.T.Fatalf("found no command with the ID %s", cmd.ID()) 330 } 331 e.ExecuteCommand(&protocol.ExecuteCommandParams{ 332 Command: lens.Command.Command, 333 Arguments: lens.Command.Arguments, 334 }, nil) 335 } 336 337 func (e *Env) ExecuteCommand(params *protocol.ExecuteCommandParams, result interface{}) { 338 e.T.Helper() 339 response, err := e.Editor.ExecuteCommand(e.Ctx, params) 340 if err != nil { 341 e.T.Fatal(err) 342 } 343 if result == nil { 344 return 345 } 346 // Hack: The result of an executeCommand request will be unmarshaled into 347 // maps. Re-marshal and unmarshal into the type we expect. 348 // 349 // This could be improved by generating a jsonrpc2 command client from the 350 // command.Interface, but that should only be done if we're consolidating 351 // this part of the tsprotocol generation. 352 data, err := json.Marshal(response) 353 if err != nil { 354 e.T.Fatal(err) 355 } 356 if err := json.Unmarshal(data, result); err != nil { 357 e.T.Fatal(err) 358 } 359 } 360 361 // WorkspaceSymbol calls workspace/symbol 362 func (e *Env) WorkspaceSymbol(sym string) []protocol.SymbolInformation { 363 e.T.Helper() 364 ans, err := e.Editor.Symbols(e.Ctx, sym) 365 if err != nil { 366 e.T.Fatal(err) 367 } 368 return ans 369 } 370 371 // References calls textDocument/references for the given path at the given 372 // position. 373 func (e *Env) References(path string, pos fake.Pos) []protocol.Location { 374 e.T.Helper() 375 locations, err := e.Editor.References(e.Ctx, path, pos) 376 if err != nil { 377 e.T.Fatal(err) 378 } 379 return locations 380 } 381 382 func (e *Env) Rename(path string, pos fake.Pos, newName string) { 383 e.T.Helper() 384 if err := e.Editor.Rename(e.Ctx, path, pos, newName); err != nil { 385 e.T.Fatal(err) 386 } 387 } 388 389 // Completion executes a completion request on the server. 390 func (e *Env) Completion(path string, pos fake.Pos) *protocol.CompletionList { 391 e.T.Helper() 392 completions, err := e.Editor.Completion(e.Ctx, path, pos) 393 if err != nil { 394 e.T.Fatal(err) 395 } 396 return completions 397 } 398 399 // AcceptCompletion accepts a completion for the given item at the given 400 // position. 401 func (e *Env) AcceptCompletion(path string, pos fake.Pos, item protocol.CompletionItem) { 402 e.T.Helper() 403 if err := e.Editor.AcceptCompletion(e.Ctx, path, pos, item); err != nil { 404 e.T.Fatal(err) 405 } 406 } 407 408 // CodeAction calls testDocument/codeAction for the given path, and calls 409 // t.Fatal if there are errors. 410 func (e *Env) CodeAction(path string, diagnostics []protocol.Diagnostic) []protocol.CodeAction { 411 e.T.Helper() 412 actions, err := e.Editor.CodeAction(e.Ctx, path, nil, diagnostics) 413 if err != nil { 414 e.T.Fatal(err) 415 } 416 return actions 417 } 418 419 func (e *Env) ChangeConfiguration(t *testing.T, config *fake.EditorConfig) { 420 e.Editor.Config = *config 421 if err := e.Editor.Server.DidChangeConfiguration(e.Ctx, &protocol.DidChangeConfigurationParams{ 422 // gopls currently ignores the Settings field 423 }); err != nil { 424 t.Fatal(err) 425 } 426 } 427 428 // ChangeEnv modifies the editor environment and reconfigures the LSP client. 429 // TODO: extend this to "ChangeConfiguration", once we refactor the way editor 430 // configuration is defined. 431 func (e *Env) ChangeEnv(overlay map[string]string) { 432 e.T.Helper() 433 // TODO: to be correct, this should probably be synchronized, but right now 434 // configuration is only ever modified synchronously in a regtest, so this 435 // correctness can wait for the previously mentioned refactoring. 436 if e.Editor.Config.Env == nil { 437 e.Editor.Config.Env = make(map[string]string) 438 } 439 for k, v := range overlay { 440 e.Editor.Config.Env[k] = v 441 } 442 var params protocol.DidChangeConfigurationParams 443 if err := e.Editor.Server.DidChangeConfiguration(e.Ctx, ¶ms); err != nil { 444 e.T.Fatal(err) 445 } 446 }