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, &params); err != nil {
442		e.T.Fatal(err)
443	}
444}
445