clerk/internal/lsp/lifecycle_test.go (view raw)
Oleksandr Smirnov
Oleksandr Smirnov
olexsmir@gmail.com lsp: comply with lsp3.18 lifecycle, 29 days ago
olexsmir@gmail.com lsp: comply with lsp3.18 lifecycle, 29 days ago
| 1 | package lsp |
| 2 | |
| 3 | import ( |
| 4 | "errors" |
| 5 | "io" |
| 6 | "os/exec" |
| 7 | "testing" |
| 8 | "time" |
| 9 | |
| 10 | "go.lsp.dev/jsonrpc2" |
| 11 | "go.lsp.dev/protocol" |
| 12 | "go.lsp.dev/uri" |
| 13 | ) |
| 14 | |
| 15 | func TestLifecycle_BeforeInitialize(t *testing.T) { |
| 16 | lct := startLifecycleTest(t) |
| 17 | // requests answer ServerNotInitialized |
| 18 | if _, err := lct.srv.Hover(t.Context(), &protocol.HoverParams{}); !codeEq(err, jsonrpc2.ServerNotInitialized) { |
| 19 | t.Fatalf("request before initialize: want -32002, got %v", err) |
| 20 | } |
| 21 | // notifications are dropped without error |
| 22 | if err := lct.srv.DidOpen(t.Context(), &protocol.DidOpenTextDocumentParams{ |
| 23 | TextDocument: protocol.TextDocumentItem{URI: uri.File("a.journal"), LanguageID: "journal", Version: 1, Text: "2024-01-01 t\n"}, |
| 24 | }); err != nil { |
| 25 | t.Fatalf("didOpen before initialize: %v", err) |
| 26 | } |
| 27 | // connection is still usable |
| 28 | if _, err := lct.srv.Initialize(t.Context(), initParams); err != nil { |
| 29 | t.Fatalf("initialize after dropped notification: %v", err) |
| 30 | } |
| 31 | } |
| 32 | |
| 33 | func TestLifecycle_InitializeMayOnlyBeSentOnce(t *testing.T) { |
| 34 | lct := startLifecycleTest(t) |
| 35 | if _, err := lct.srv.Initialize(t.Context(), initParams); err != nil { |
| 36 | t.Fatalf("first initialize: %v", err) |
| 37 | } |
| 38 | if _, err := lct.srv.Initialize(t.Context(), initParams); !codeEq(err, jsonrpc2.InvalidRequest) { |
| 39 | t.Fatalf("second initialize: want -32600, got %v", err) |
| 40 | } |
| 41 | } |
| 42 | |
| 43 | func TestLifecycle_RequestsServedAfterInitialize(t *testing.T) { |
| 44 | lct := startLifecycleTest(t) |
| 45 | if _, err := lct.srv.Initialize(t.Context(), initParams); err != nil { |
| 46 | t.Fatal(err) |
| 47 | } |
| 48 | // a request in the running phase must reach the real handler: an unopened |
| 49 | // document yields an empty semantic-tokens result, never a lifecycle error. |
| 50 | if _, err := lct.srv.SemanticTokensFull(t.Context(), &protocol.SemanticTokensParams{ |
| 51 | TextDocument: protocol.TextDocumentIdentifier{URI: uri.File("nowhere.journal")}, |
| 52 | }); err != nil { |
| 53 | t.Fatalf("request after initialize: %v", err) |
| 54 | } |
| 55 | } |
| 56 | |
| 57 | func TestLifecycle_ExitCodes(t *testing.T) { |
| 58 | for tname, tc := range map[string]struct { |
| 59 | wantCode int |
| 60 | shutdown, init bool |
| 61 | }{ |
| 62 | "exit before initialize": {wantCode: 1}, |
| 63 | "exit after initialize": {wantCode: 1, init: true}, |
| 64 | "exit after shutdown": {wantCode: 0, init: true, shutdown: true}, |
| 65 | } { |
| 66 | t.Run(tname, func(t *testing.T) { |
| 67 | lct := startLifecycleTest(t) |
| 68 | if tc.init { |
| 69 | if _, err := lct.srv.Initialize(t.Context(), initParams); err != nil { |
| 70 | t.Fatal(err) |
| 71 | } |
| 72 | } |
| 73 | if tc.shutdown { |
| 74 | if err := lct.srv.Shutdown(t.Context()); err != nil { |
| 75 | t.Fatal(err) |
| 76 | } |
| 77 | // like gopls, the server trusts its client: a request after |
| 78 | // shutdown is still served (a nonexistent document yields an |
| 79 | // empty hover), never a lifecycle error. |
| 80 | if _, err := lct.srv.Hover(t.Context(), &protocol.HoverParams{}); err != nil { |
| 81 | t.Fatalf("request after shutdown: %v", err) |
| 82 | } |
| 83 | } |
| 84 | if err := lct.srv.Exit(t.Context()); err != nil { |
| 85 | t.Fatal(err) |
| 86 | } |
| 87 | var ee *ExitError |
| 88 | if err := <-lct.errc; !errors.As(err, &ee) || ee.Code != tc.wantCode { |
| 89 | t.Fatalf("want code %d, got %v", tc.wantCode, err) |
| 90 | } |
| 91 | }) |
| 92 | } |
| 93 | } |
| 94 | |
| 95 | func TestLifecycle_ParentDeathExits(t *testing.T) { |
| 96 | if testing.Short() { |
| 97 | t.Skip("spawns a subprocess") |
| 98 | } |
| 99 | |
| 100 | child := exec.Command("sleep", "30") |
| 101 | if err := child.Start(); err != nil { |
| 102 | t.Fatal(err) |
| 103 | } |
| 104 | pid := int32(child.Process.Pid) |
| 105 | child.Process.Kill() |
| 106 | child.Wait() // reap so the immediate signal-0 probe sees ESRCH |
| 107 | |
| 108 | lct := startLifecycleTest(t) |
| 109 | |
| 110 | // initialize in the background: watchParent's immediate probe fires while |
| 111 | // the request is still being handled, so its response is dropped and the |
| 112 | // server exits before the call returns. |
| 113 | go func() { lct.srv.Initialize(t.Context(), &protocol.InitializeParams{ProcessID: &pid}) }() |
| 114 | |
| 115 | select { |
| 116 | case err := <-lct.errc: |
| 117 | var ee *ExitError |
| 118 | if !errors.As(err, &ee) || ee.Code != 1 { |
| 119 | t.Fatalf("parent death: want code 1, got %v", err) |
| 120 | } |
| 121 | case <-time.After(5 * time.Second): |
| 122 | t.Fatal("server did not exit after parent death") |
| 123 | } |
| 124 | } |
| 125 | |
| 126 | var initParams = &protocol.InitializeParams{ |
| 127 | WorkspaceFoldersInitializeParams: protocol.WorkspaceFoldersInitializeParams{}, |
| 128 | Capabilities: protocol.ClientCapabilities{}, |
| 129 | } |
| 130 | |
| 131 | func codeEq(err error, code jsonrpc2.Code) bool { |
| 132 | return errors.Is(err, jsonrpc2.NewError(code, "")) |
| 133 | } |
| 134 | |
| 135 | type lifecycleTest struct { |
| 136 | errc chan error |
| 137 | srv protocol.Server |
| 138 | } |
| 139 | |
| 140 | func startLifecycleTest(t *testing.T) *lifecycleTest { |
| 141 | t.Helper() |
| 142 | s := newServer(t) |
| 143 | lct := &lifecycleTest{errc: make(chan error, 1)} |
| 144 | inR, inW := io.Pipe() // client -> server |
| 145 | outR, outW := io.Pipe() // server -> client |
| 146 | |
| 147 | go func() { lct.errc <- s.Run(t.Context(), inR, outW) }() |
| 148 | t.Cleanup(func() { |
| 149 | inW.Close() |
| 150 | outR.Close() |
| 151 | }) |
| 152 | |
| 153 | cconn := jsonrpc2.NewConn(jsonrpc2.NewStream(readWriterCloser{Reader: outR, Writer: inW, Closer: outR}), jsonrpc2.WithCodec(lspCodec{})) |
| 154 | lct.srv = protocol.ServerDispatcher(cconn) |
| 155 | cconn.Go(t.Context(), protocol.Handlers(protocol.ClientHandler(&captureClient{}, jsonrpc2.MethodNotFoundHandler))) |
| 156 | return lct |
| 157 | } |