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