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