all repos

clerk @ 78d7e88

missing tooling for ledger/hledger

clerk/internal/lsp/lifecycle_test.go (view raw)

Oleksandr Smirnov Oleksandr Smirnov
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
}