github.com/v2fly/tools@v0.100.0/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  	"io"
    10  	"path"
    11  	"testing"
    12  
    13  	"github.com/v2fly/tools/internal/lsp/command"
    14  	"github.com/v2fly/tools/internal/lsp/fake"
    15  	"github.com/v2fly/tools/internal/lsp/protocol"
    16  	errors "golang.org/x/xerrors"
    17  )
    18  
    19  func (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.
    28  func (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.
    37  func (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.
    48  func (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.
    57  func (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.
    65  func (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.
    73  func (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.
    82  func (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.
    90  func (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  
    97  func (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.
   107  func (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.
   122  func (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.
   136  func (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.
   144  func (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  
   151  func (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.
   160  func (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
   170  func (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.
   180  func (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.
   189  func (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.
   197  func (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  // GetQuickFixes returns the available quick fix code actions.
   205  func (e *Env) GetQuickFixes(path string, diagnostics []protocol.Diagnostic) []protocol.CodeAction {
   206  	e.T.Helper()
   207  	actions, err := e.Editor.GetQuickFixes(e.Ctx, path, nil, diagnostics)
   208  	if err != nil {
   209  		e.T.Fatal(err)
   210  	}
   211  	return actions
   212  }
   213  
   214  // Hover in the editor, calling t.Fatal on any error.
   215  func (e *Env) Hover(name string, pos fake.Pos) (*protocol.MarkupContent, fake.Pos) {
   216  	e.T.Helper()
   217  	c, p, err := e.Editor.Hover(e.Ctx, name, pos)
   218  	if err != nil {
   219  		e.T.Fatal(err)
   220  	}
   221  	return c, p
   222  }
   223  
   224  func (e *Env) DocumentLink(name string) []protocol.DocumentLink {
   225  	e.T.Helper()
   226  	links, err := e.Editor.DocumentLink(e.Ctx, name)
   227  	if err != nil {
   228  		e.T.Fatal(err)
   229  	}
   230  	return links
   231  }
   232  
   233  func (e *Env) DocumentHighlight(name string, pos fake.Pos) []protocol.DocumentHighlight {
   234  	e.T.Helper()
   235  	highlights, err := e.Editor.DocumentHighlight(e.Ctx, name, pos)
   236  	if err != nil {
   237  		e.T.Fatal(err)
   238  	}
   239  	return highlights
   240  }
   241  
   242  func checkIsFatal(t *testing.T, err error) {
   243  	t.Helper()
   244  	if err != nil && !errors.Is(err, io.EOF) && !errors.Is(err, io.ErrClosedPipe) {
   245  		t.Fatal(err)
   246  	}
   247  }
   248  
   249  // CloseEditor shuts down the editor, calling t.Fatal on any error.
   250  func (e *Env) CloseEditor() {
   251  	e.T.Helper()
   252  	checkIsFatal(e.T, e.Editor.Close(e.Ctx))
   253  }
   254  
   255  // RunGenerate runs go:generate on the given dir, calling t.Fatal on any error.
   256  // It waits for the generate command to complete and checks for file changes
   257  // before returning.
   258  func (e *Env) RunGenerate(dir string) {
   259  	e.T.Helper()
   260  	if err := e.Editor.RunGenerate(e.Ctx, dir); err != nil {
   261  		e.T.Fatal(err)
   262  	}
   263  	e.Await(NoOutstandingWork())
   264  	// Ideally the fake.Workspace would handle all synthetic file watching, but
   265  	// we help it out here as we need to wait for the generate command to
   266  	// complete before checking the filesystem.
   267  	e.CheckForFileChanges()
   268  }
   269  
   270  // RunGoCommand runs the given command in the sandbox's default working
   271  // directory.
   272  func (e *Env) RunGoCommand(verb string, args ...string) {
   273  	e.T.Helper()
   274  	if err := e.Sandbox.RunGoCommand(e.Ctx, "", verb, args); err != nil {
   275  		e.T.Fatal(err)
   276  	}
   277  }
   278  
   279  // RunGoCommandInDir is like RunGoCommand, but executes in the given
   280  // relative directory of the sandbox.
   281  func (e *Env) RunGoCommandInDir(dir, verb string, args ...string) {
   282  	e.T.Helper()
   283  	if err := e.Sandbox.RunGoCommand(e.Ctx, dir, verb, args); err != nil {
   284  		e.T.Fatal(err)
   285  	}
   286  }
   287  
   288  // DumpGoSum prints the correct go.sum contents for dir in txtar format,
   289  // for use in creating regtests.
   290  func (e *Env) DumpGoSum(dir string) {
   291  	e.T.Helper()
   292  
   293  	if err := e.Sandbox.RunGoCommand(e.Ctx, dir, "list", []string{"-mod=mod", "..."}); err != nil {
   294  		e.T.Fatal(err)
   295  	}
   296  	sumFile := path.Join(dir, "/go.sum")
   297  	e.T.Log("\n\n-- " + sumFile + " --\n" + e.ReadWorkspaceFile(sumFile))
   298  	e.T.Fatal("see contents above")
   299  }
   300  
   301  // CheckForFileChanges triggers a manual poll of the workspace for any file
   302  // changes since creation, or since last polling. It is a workaround for the
   303  // lack of true file watching support in the fake workspace.
   304  func (e *Env) CheckForFileChanges() {
   305  	e.T.Helper()
   306  	if err := e.Sandbox.Workdir.CheckForFileChanges(e.Ctx); err != nil {
   307  		e.T.Fatal(err)
   308  	}
   309  }
   310  
   311  // CodeLens calls textDocument/codeLens for the given path, calling t.Fatal on
   312  // any error.
   313  func (e *Env) CodeLens(path string) []protocol.CodeLens {
   314  	e.T.Helper()
   315  	lens, err := e.Editor.CodeLens(e.Ctx, path)
   316  	if err != nil {
   317  		e.T.Fatal(err)
   318  	}
   319  	return lens
   320  }
   321  
   322  // ExecuteCodeLensCommand executes the command for the code lens matching the
   323  // given command name.
   324  func (e *Env) ExecuteCodeLensCommand(path string, cmd command.Command) {
   325  	e.T.Helper()
   326  	lenses := e.CodeLens(path)
   327  	var lens protocol.CodeLens
   328  	var found bool
   329  	for _, l := range lenses {
   330  		if l.Command.Command == cmd.ID() {
   331  			lens = l
   332  			found = true
   333  		}
   334  	}
   335  	if !found {
   336  		e.T.Fatalf("found no command with the ID %s", cmd.ID())
   337  	}
   338  	e.ExecuteCommand(&protocol.ExecuteCommandParams{
   339  		Command:   lens.Command.Command,
   340  		Arguments: lens.Command.Arguments,
   341  	}, nil)
   342  }
   343  
   344  func (e *Env) ExecuteCommand(params *protocol.ExecuteCommandParams, result interface{}) {
   345  	e.T.Helper()
   346  	response, err := e.Editor.ExecuteCommand(e.Ctx, params)
   347  	if err != nil {
   348  		e.T.Fatal(err)
   349  	}
   350  	if result == nil {
   351  		return
   352  	}
   353  	// Hack: The result of an executeCommand request will be unmarshaled into
   354  	// maps. Re-marshal and unmarshal into the type we expect.
   355  	//
   356  	// This could be improved by generating a jsonrpc2 command client from the
   357  	// command.Interface, but that should only be done if we're consolidating
   358  	// this part of the tsprotocol generation.
   359  	data, err := json.Marshal(response)
   360  	if err != nil {
   361  		e.T.Fatal(err)
   362  	}
   363  	if err := json.Unmarshal(data, result); err != nil {
   364  		e.T.Fatal(err)
   365  	}
   366  }
   367  
   368  // References calls textDocument/references for the given path at the given
   369  // position.
   370  func (e *Env) References(path string, pos fake.Pos) []protocol.Location {
   371  	e.T.Helper()
   372  	locations, err := e.Editor.References(e.Ctx, path, pos)
   373  	if err != nil {
   374  		e.T.Fatal(err)
   375  	}
   376  	return locations
   377  }
   378  
   379  // Completion executes a completion request on the server.
   380  func (e *Env) Completion(path string, pos fake.Pos) *protocol.CompletionList {
   381  	e.T.Helper()
   382  	completions, err := e.Editor.Completion(e.Ctx, path, pos)
   383  	if err != nil {
   384  		e.T.Fatal(err)
   385  	}
   386  	return completions
   387  }
   388  
   389  // AcceptCompletion accepts a completion for the given item at the given
   390  // position.
   391  func (e *Env) AcceptCompletion(path string, pos fake.Pos, item protocol.CompletionItem) {
   392  	e.T.Helper()
   393  	if err := e.Editor.AcceptCompletion(e.Ctx, path, pos, item); err != nil {
   394  		e.T.Fatal(err)
   395  	}
   396  }
   397  
   398  // CodeAction calls testDocument/codeAction for the given path, and calls
   399  // t.Fatal if there are errors.
   400  func (e *Env) CodeAction(path string, diagnostics []protocol.Diagnostic) []protocol.CodeAction {
   401  	e.T.Helper()
   402  	actions, err := e.Editor.CodeAction(e.Ctx, path, nil, diagnostics)
   403  	if err != nil {
   404  		e.T.Fatal(err)
   405  	}
   406  	return actions
   407  }
   408  
   409  func (e *Env) ChangeConfiguration(t *testing.T, config *fake.EditorConfig) {
   410  	e.Editor.Config = *config
   411  	if err := e.Editor.Server.DidChangeConfiguration(e.Ctx, &protocol.DidChangeConfigurationParams{
   412  		// gopls currently ignores the Settings field
   413  	}); err != nil {
   414  		t.Fatal(err)
   415  	}
   416  }
   417  
   418  // ChangeEnv modifies the editor environment and reconfigures the LSP client.
   419  // TODO: extend this to "ChangeConfiguration", once we refactor the way editor
   420  // configuration is defined.
   421  func (e *Env) ChangeEnv(overlay map[string]string) {
   422  	e.T.Helper()
   423  	// TODO: to be correct, this should probably be synchronized, but right now
   424  	// configuration is only ever modified synchronously in a regtest, so this
   425  	// correctness can wait for the previously mentioned refactoring.
   426  	if e.Editor.Config.Env == nil {
   427  		e.Editor.Config.Env = make(map[string]string)
   428  	}
   429  	for k, v := range overlay {
   430  		e.Editor.Config.Env[k] = v
   431  	}
   432  	var params protocol.DidChangeConfigurationParams
   433  	if err := e.Editor.Server.DidChangeConfiguration(e.Ctx, &params); err != nil {
   434  		e.T.Fatal(err)
   435  	}
   436  }