lsp_test.go 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
  1. package lsp
  2. import (
  3. "context"
  4. "encoding/json"
  5. "fmt"
  6. "os"
  7. "reflect"
  8. "testing"
  9. "time"
  10. lsp "github.com/sourcegraph/go-lsp"
  11. "github.com/sourcegraph/jsonrpc2"
  12. "src.elv.sh/pkg/prog"
  13. "src.elv.sh/pkg/prog/progtest"
  14. "src.elv.sh/pkg/testutil"
  15. )
  16. var diagTests = []struct {
  17. name string
  18. text string
  19. wantDiags []lsp.Diagnostic
  20. }{
  21. {"empty", "", []lsp.Diagnostic{}},
  22. {"no error", "echo", []lsp.Diagnostic{}},
  23. {"single error", "$!", []lsp.Diagnostic{
  24. {
  25. Range: lsp.Range{
  26. Start: lsp.Position{Line: 0, Character: 1},
  27. End: lsp.Position{Line: 0, Character: 2}},
  28. Severity: lsp.Error, Source: "parse", Message: "should be variable name",
  29. },
  30. }},
  31. {"multi line with NL", "\n$!", []lsp.Diagnostic{
  32. {
  33. Range: lsp.Range{
  34. Start: lsp.Position{Line: 1, Character: 1},
  35. End: lsp.Position{Line: 1, Character: 2}},
  36. Severity: lsp.Error, Source: "parse", Message: "should be variable name",
  37. },
  38. }},
  39. {"multi line with CR", "\r$!", []lsp.Diagnostic{
  40. {
  41. Range: lsp.Range{
  42. Start: lsp.Position{Line: 1, Character: 1},
  43. End: lsp.Position{Line: 1, Character: 2}},
  44. Severity: lsp.Error, Source: "parse", Message: "should be variable name",
  45. },
  46. }},
  47. {"multi line with CRNL", "\r\n$!", []lsp.Diagnostic{
  48. {
  49. Range: lsp.Range{
  50. Start: lsp.Position{Line: 1, Character: 1},
  51. End: lsp.Position{Line: 1, Character: 2}},
  52. Severity: lsp.Error, Source: "parse", Message: "should be variable name",
  53. },
  54. }},
  55. {"text with code point beyond FFFF", "\U00010000 $!", []lsp.Diagnostic{
  56. {
  57. Range: lsp.Range{
  58. Start: lsp.Position{Line: 0, Character: 4},
  59. End: lsp.Position{Line: 0, Character: 5}},
  60. Severity: lsp.Error, Source: "parse", Message: "should be variable name",
  61. },
  62. }},
  63. }
  64. func TestDidOpenDiagnostics(t *testing.T) {
  65. f := setup(t)
  66. for _, test := range diagTests {
  67. t.Run(test.name, func(t *testing.T) {
  68. f.conn.Notify(context.Background(),
  69. "textDocument/didOpen", didOpenParams(test.text))
  70. checkDiag(t, f, diagParam(test.wantDiags))
  71. })
  72. }
  73. }
  74. func TestDidChangeDiagnostics(t *testing.T) {
  75. f := setup(t)
  76. f.conn.Notify(context.Background(), "textDocument/didOpen", didOpenParams(""))
  77. checkDiag(t, f, diagParam([]lsp.Diagnostic{}))
  78. for _, test := range diagTests {
  79. t.Run(test.name, func(t *testing.T) {
  80. f.conn.Notify(context.Background(),
  81. "textDocument/didChange", didChangeParams(test.text))
  82. checkDiag(t, f, diagParam(test.wantDiags))
  83. })
  84. }
  85. }
  86. var jsonrpcErrorTests = []struct {
  87. method string
  88. params interface{}
  89. wantErr error
  90. }{
  91. {"unknown/method", struct{}{}, errMethodNotFound},
  92. {"textDocument/didOpen", []int{}, errInvalidParams},
  93. {"textDocument/didChange", []int{}, errInvalidParams},
  94. }
  95. func TestJSONRPCErrors(t *testing.T) {
  96. f := setup(t)
  97. for _, test := range jsonrpcErrorTests {
  98. t.Run(test.method, func(t *testing.T) {
  99. err := f.conn.Call(context.Background(), test.method, test.params, &struct{}{})
  100. if err.Error() != test.wantErr.Error() {
  101. t.Errorf("got error %v, want %v", err, errMethodNotFound)
  102. }
  103. })
  104. }
  105. }
  106. func TestProgramErrors(t *testing.T) {
  107. progtest.Test(t, &Program{},
  108. progtest.ThatElvish("").
  109. ExitsWith(2).
  110. WritesStderr("internal error: no suitable subprogram\n"))
  111. }
  112. const testURI = "file:///foo"
  113. func didOpenParams(text string) lsp.DidOpenTextDocumentParams {
  114. return lsp.DidOpenTextDocumentParams{
  115. TextDocument: lsp.TextDocumentItem{URI: testURI, Text: text}}
  116. }
  117. func didChangeParams(text string) lsp.DidChangeTextDocumentParams {
  118. return lsp.DidChangeTextDocumentParams{
  119. TextDocument: lsp.VersionedTextDocumentIdentifier{
  120. TextDocumentIdentifier: lsp.TextDocumentIdentifier{URI: testURI},
  121. },
  122. ContentChanges: []lsp.TextDocumentContentChangeEvent{
  123. {Text: text},
  124. }}
  125. }
  126. func diagParam(diags []lsp.Diagnostic) lsp.PublishDiagnosticsParams {
  127. return lsp.PublishDiagnosticsParams{URI: testURI, Diagnostics: diags}
  128. }
  129. func checkDiag(t *testing.T, f *clientFixture, want lsp.PublishDiagnosticsParams) {
  130. t.Helper()
  131. select {
  132. case got := <-f.diags:
  133. if !reflect.DeepEqual(got, want) {
  134. t.Errorf("got %v, want %v", got, want)
  135. }
  136. case <-time.After(testutil.Scaled(time.Second)):
  137. t.Errorf("time out")
  138. }
  139. }
  140. type clientFixture struct {
  141. conn *jsonrpc2.Conn
  142. diags <-chan lsp.PublishDiagnosticsParams
  143. }
  144. func setup(t *testing.T) *clientFixture {
  145. r0, w0 := testutil.MustPipe()
  146. r1, w1 := testutil.MustPipe()
  147. // Run server
  148. done := make(chan struct{})
  149. go func() {
  150. prog.Run([3]*os.File{r0, w1, nil}, []string{"elvish", "-lsp"}, &Program{})
  151. close(done)
  152. }()
  153. t.Cleanup(func() { <-done })
  154. // Run client
  155. diags := make(chan lsp.PublishDiagnosticsParams, 100)
  156. client := client{diags}
  157. conn := jsonrpc2.NewConn(context.Background(),
  158. jsonrpc2.NewBufferedStream(transport{r1, w0}, jsonrpc2.VSCodeObjectCodec{}),
  159. client.handler())
  160. t.Cleanup(func() { conn.Close() })
  161. // LSP handshake
  162. err := conn.Call(context.Background(),
  163. "initialize", lsp.InitializeParams{}, &lsp.InitializeResult{})
  164. if err != nil {
  165. t.Errorf("got error %v, want nil", err)
  166. }
  167. err = conn.Notify(context.Background(), "initialized", struct{}{})
  168. if err != nil {
  169. t.Errorf("got error %v, want nil", err)
  170. }
  171. return &clientFixture{conn, diags}
  172. }
  173. type client struct {
  174. diags chan<- lsp.PublishDiagnosticsParams
  175. }
  176. func (c *client) handler() jsonrpc2.Handler {
  177. return routingHandler(map[string]method{
  178. "textDocument/publishDiagnostics": c.publishDiagnostics,
  179. })
  180. }
  181. func (c *client) publishDiagnostics(_ context.Context, _ jsonrpc2.JSONRPC2, rawParams json.RawMessage) (interface{}, error) {
  182. var params lsp.PublishDiagnosticsParams
  183. err := json.Unmarshal(rawParams, &params)
  184. if err != nil {
  185. panic(fmt.Sprintf("parse PublishDiagnosticsParams: %v", err))
  186. }
  187. c.diags <- params
  188. return nil, nil
  189. }