github.com/v2fly/tools@v0.100.0/internal/lsp/lsprpc/lsprpc_test.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 lsprpc
     6  
     7  import (
     8  	"context"
     9  	"regexp"
    10  	"sync"
    11  	"testing"
    12  	"time"
    13  
    14  	"github.com/v2fly/tools/internal/event"
    15  	"github.com/v2fly/tools/internal/jsonrpc2"
    16  	"github.com/v2fly/tools/internal/jsonrpc2/servertest"
    17  	"github.com/v2fly/tools/internal/lsp/cache"
    18  	"github.com/v2fly/tools/internal/lsp/debug"
    19  	"github.com/v2fly/tools/internal/lsp/fake"
    20  	"github.com/v2fly/tools/internal/lsp/protocol"
    21  	"github.com/v2fly/tools/internal/testenv"
    22  )
    23  
    24  type fakeClient struct {
    25  	protocol.Client
    26  
    27  	logs chan string
    28  }
    29  
    30  func (c fakeClient) LogMessage(ctx context.Context, params *protocol.LogMessageParams) error {
    31  	c.logs <- params.Message
    32  	return nil
    33  }
    34  
    35  // fakeServer is intended to be embedded in the test fakes below, to trivially
    36  // implement Shutdown.
    37  type fakeServer struct {
    38  	protocol.Server
    39  }
    40  
    41  func (fakeServer) Shutdown(ctx context.Context) error {
    42  	return nil
    43  }
    44  
    45  type pingServer struct{ fakeServer }
    46  
    47  func (s pingServer) DidOpen(ctx context.Context, params *protocol.DidOpenTextDocumentParams) error {
    48  	event.Log(ctx, "ping")
    49  	return nil
    50  }
    51  
    52  func TestClientLogging(t *testing.T) {
    53  	ctx, cancel := context.WithCancel(context.Background())
    54  	defer cancel()
    55  
    56  	server := pingServer{}
    57  	client := fakeClient{logs: make(chan string, 10)}
    58  
    59  	ctx = debug.WithInstance(ctx, "", "")
    60  	ss := NewStreamServer(cache.New(nil), false)
    61  	ss.serverForTest = server
    62  	ts := servertest.NewPipeServer(ctx, ss, nil)
    63  	defer checkClose(t, ts.Close)
    64  	cc := ts.Connect(ctx)
    65  	cc.Go(ctx, protocol.ClientHandler(client, jsonrpc2.MethodNotFound))
    66  
    67  	protocol.ServerDispatcher(cc).DidOpen(ctx, &protocol.DidOpenTextDocumentParams{})
    68  
    69  	select {
    70  	case got := <-client.logs:
    71  		want := "ping"
    72  		matched, err := regexp.MatchString(want, got)
    73  		if err != nil {
    74  			t.Fatal(err)
    75  		}
    76  		if !matched {
    77  			t.Errorf("got log %q, want a log containing %q", got, want)
    78  		}
    79  	case <-time.After(1 * time.Second):
    80  		t.Error("timeout waiting for client log")
    81  	}
    82  }
    83  
    84  // waitableServer instruments LSP request so that we can control their timing.
    85  // The requests chosen are arbitrary: we simply needed one that blocks, and
    86  // another that doesn't.
    87  type waitableServer struct {
    88  	fakeServer
    89  
    90  	started chan struct{}
    91  }
    92  
    93  func (s waitableServer) Hover(ctx context.Context, _ *protocol.HoverParams) (*protocol.Hover, error) {
    94  	s.started <- struct{}{}
    95  	select {
    96  	case <-ctx.Done():
    97  		return nil, ctx.Err()
    98  	case <-time.After(200 * time.Millisecond):
    99  	}
   100  	return &protocol.Hover{}, nil
   101  }
   102  
   103  func (s waitableServer) Resolve(_ context.Context, item *protocol.CompletionItem) (*protocol.CompletionItem, error) {
   104  	return item, nil
   105  }
   106  
   107  func checkClose(t *testing.T, closer func() error) {
   108  	t.Helper()
   109  	if err := closer(); err != nil {
   110  		t.Errorf("closing: %v", err)
   111  	}
   112  }
   113  
   114  func setupForwarding(ctx context.Context, t *testing.T, s protocol.Server) (direct, forwarded servertest.Connector, cleanup func()) {
   115  	t.Helper()
   116  	serveCtx := debug.WithInstance(ctx, "", "")
   117  	ss := NewStreamServer(cache.New(nil), false)
   118  	ss.serverForTest = s
   119  	tsDirect := servertest.NewTCPServer(serveCtx, ss, nil)
   120  
   121  	forwarderCtx := debug.WithInstance(ctx, "", "")
   122  	forwarder := NewForwarder("tcp", tsDirect.Addr)
   123  	tsForwarded := servertest.NewPipeServer(forwarderCtx, forwarder, nil)
   124  	return tsDirect, tsForwarded, func() {
   125  		checkClose(t, tsDirect.Close)
   126  		checkClose(t, tsForwarded.Close)
   127  	}
   128  }
   129  
   130  func TestRequestCancellation(t *testing.T) {
   131  	ctx := context.Background()
   132  	server := waitableServer{
   133  		started: make(chan struct{}),
   134  	}
   135  	tsDirect, tsForwarded, cleanup := setupForwarding(ctx, t, server)
   136  	defer cleanup()
   137  	tests := []struct {
   138  		serverType string
   139  		ts         servertest.Connector
   140  	}{
   141  		{"direct", tsDirect},
   142  		{"forwarder", tsForwarded},
   143  	}
   144  
   145  	for _, test := range tests {
   146  		t.Run(test.serverType, func(t *testing.T) {
   147  			cc := test.ts.Connect(ctx)
   148  			sd := protocol.ServerDispatcher(cc)
   149  			cc.Go(ctx,
   150  				protocol.Handlers(
   151  					jsonrpc2.MethodNotFound))
   152  
   153  			ctx := context.Background()
   154  			ctx1, cancel1 := context.WithCancel(ctx)
   155  			var (
   156  				err1, err2 error
   157  				wg         sync.WaitGroup
   158  			)
   159  			wg.Add(2)
   160  			go func() {
   161  				defer wg.Done()
   162  				_, err1 = sd.Hover(ctx1, &protocol.HoverParams{})
   163  			}()
   164  			go func() {
   165  				defer wg.Done()
   166  				_, err2 = sd.Resolve(ctx, &protocol.CompletionItem{})
   167  			}()
   168  			// Wait for the Hover request to start.
   169  			<-server.started
   170  			cancel1()
   171  			wg.Wait()
   172  			if err1 == nil {
   173  				t.Errorf("cancelled Hover(): got nil err")
   174  			}
   175  			if err2 != nil {
   176  				t.Errorf("uncancelled Hover(): err: %v", err2)
   177  			}
   178  			if _, err := sd.Resolve(ctx, &protocol.CompletionItem{}); err != nil {
   179  				t.Errorf("subsequent Hover(): %v", err)
   180  			}
   181  		})
   182  	}
   183  }
   184  
   185  const exampleProgram = `
   186  -- go.mod --
   187  module mod
   188  
   189  go 1.12
   190  -- main.go --
   191  package main
   192  
   193  import "fmt"
   194  
   195  func main() {
   196  	fmt.Println("Hello World.")
   197  }`
   198  
   199  func TestDebugInfoLifecycle(t *testing.T) {
   200  	sb, err := fake.NewSandbox(&fake.SandboxConfig{Files: exampleProgram})
   201  	if err != nil {
   202  		t.Fatal(err)
   203  	}
   204  	defer func() {
   205  		if err := sb.Close(); err != nil {
   206  			// TODO(golang/go#38490): we can't currently make this an error because
   207  			// it fails on Windows: the workspace directory is still locked by a
   208  			// separate Go process.
   209  			// Once we have a reliable way to wait for proper shutdown, make this an
   210  			// error.
   211  			t.Logf("closing workspace failed: %v", err)
   212  		}
   213  	}()
   214  
   215  	baseCtx, cancel := context.WithCancel(context.Background())
   216  	defer cancel()
   217  	clientCtx := debug.WithInstance(baseCtx, "", "")
   218  	serverCtx := debug.WithInstance(baseCtx, "", "")
   219  
   220  	cache := cache.New(nil)
   221  	ss := NewStreamServer(cache, false)
   222  	tsBackend := servertest.NewTCPServer(serverCtx, ss, nil)
   223  
   224  	forwarder := NewForwarder("tcp", tsBackend.Addr)
   225  	tsForwarder := servertest.NewPipeServer(clientCtx, forwarder, nil)
   226  
   227  	conn1 := tsForwarder.Connect(clientCtx)
   228  	ed1, err := fake.NewEditor(sb, fake.EditorConfig{}).Connect(clientCtx, conn1, fake.ClientHooks{})
   229  	if err != nil {
   230  		t.Fatal(err)
   231  	}
   232  	defer ed1.Close(clientCtx)
   233  	conn2 := tsBackend.Connect(baseCtx)
   234  	ed2, err := fake.NewEditor(sb, fake.EditorConfig{}).Connect(baseCtx, conn2, fake.ClientHooks{})
   235  	if err != nil {
   236  		t.Fatal(err)
   237  	}
   238  	defer ed2.Close(baseCtx)
   239  
   240  	serverDebug := debug.GetInstance(serverCtx)
   241  	if got, want := len(serverDebug.State.Clients()), 2; got != want {
   242  		t.Errorf("len(server:Clients) = %d, want %d", got, want)
   243  	}
   244  	if got, want := len(serverDebug.State.Sessions()), 2; got != want {
   245  		t.Errorf("len(server:Sessions) = %d, want %d", got, want)
   246  	}
   247  	clientDebug := debug.GetInstance(clientCtx)
   248  	if got, want := len(clientDebug.State.Servers()), 1; got != want {
   249  		t.Errorf("len(client:Servers) = %d, want %d", got, want)
   250  	}
   251  	// Close one of the connections to verify that the client and session were
   252  	// dropped.
   253  	if err := ed1.Close(clientCtx); err != nil {
   254  		t.Fatal(err)
   255  	}
   256  	/*TODO: at this point we have verified the editor is closed
   257  	However there is no way currently to wait for all associated go routines to
   258  	go away, and we need to wait for those to trigger the client drop
   259  	for now we just give it a little bit of time, but we need to fix this
   260  	in a principled way
   261  	*/
   262  	start := time.Now()
   263  	delay := time.Millisecond
   264  	const maxWait = time.Second
   265  	for len(serverDebug.State.Clients()) > 1 {
   266  		if time.Since(start) > maxWait {
   267  			break
   268  		}
   269  		time.Sleep(delay)
   270  		delay *= 2
   271  	}
   272  	if got, want := len(serverDebug.State.Clients()), 1; got != want {
   273  		t.Errorf("len(server:Clients) = %d, want %d", got, want)
   274  	}
   275  	if got, want := len(serverDebug.State.Sessions()), 1; got != want {
   276  		t.Errorf("len(server:Sessions()) = %d, want %d", got, want)
   277  	}
   278  }
   279  
   280  type initServer struct {
   281  	fakeServer
   282  
   283  	params *protocol.ParamInitialize
   284  }
   285  
   286  func (s *initServer) Initialize(ctx context.Context, params *protocol.ParamInitialize) (*protocol.InitializeResult, error) {
   287  	s.params = params
   288  	return &protocol.InitializeResult{}, nil
   289  }
   290  
   291  func TestEnvForwarding(t *testing.T) {
   292  	testenv.NeedsGo1Point(t, 13)
   293  	server := &initServer{}
   294  	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
   295  	defer cancel()
   296  	_, tsForwarded, cleanup := setupForwarding(ctx, t, server)
   297  	defer cleanup()
   298  
   299  	conn := tsForwarded.Connect(ctx)
   300  	conn.Go(ctx, jsonrpc2.MethodNotFound)
   301  	dispatch := protocol.ServerDispatcher(conn)
   302  	initParams := &protocol.ParamInitialize{}
   303  	initParams.InitializationOptions = map[string]interface{}{
   304  		"env": map[string]interface{}{
   305  			"GONOPROXY": "example.com",
   306  		},
   307  	}
   308  	_, err := dispatch.Initialize(ctx, initParams)
   309  	if err != nil {
   310  		t.Fatal(err)
   311  	}
   312  	if server.params == nil {
   313  		t.Fatalf("initialize params are unset")
   314  	}
   315  	env := server.params.InitializationOptions.(map[string]interface{})["env"].(map[string]interface{})
   316  
   317  	// Check for an arbitrary Go variable. It should be set.
   318  	if _, ok := env["GOPRIVATE"]; !ok {
   319  		t.Errorf("Go environment variable GOPRIVATE unset in initialization options")
   320  	}
   321  	// Check that the variable present in our user config was not overwritten.
   322  	if v := env["GONOPROXY"]; v != "example.com" {
   323  		t.Errorf("GONOPROXY environment variable was overwritten")
   324  	}
   325  }
   326  
   327  func TestListenParsing(t *testing.T) {
   328  	tests := []struct {
   329  		input, wantNetwork, wantAddr string
   330  	}{
   331  		{"127.0.0.1:0", "tcp", "127.0.0.1:0"},
   332  		{"unix;/tmp/sock", "unix", "/tmp/sock"},
   333  		{"auto", "auto", ""},
   334  		{"auto;foo", "auto", "foo"},
   335  	}
   336  
   337  	for _, test := range tests {
   338  		gotNetwork, gotAddr := ParseAddr(test.input)
   339  		if gotNetwork != test.wantNetwork {
   340  			t.Errorf("network = %q, want %q", gotNetwork, test.wantNetwork)
   341  		}
   342  		if gotAddr != test.wantAddr {
   343  			t.Errorf("addr = %q, want %q", gotAddr, test.wantAddr)
   344  		}
   345  	}
   346  }