Class: Pikuri::Lsp::Testing::Wire

Inherits:
Object
  • Object
show all
Defined in:
lib/pikuri/lsp/testing.rb

Overview

The server side of one LSP channel: hand-rolled framing over a pair of pipes, blocking reads, and no behaviour of its own.

Drive it from the example's thread with the client under test on a worker (or use FakeServer). A spec that writes more than the pipe buffer with nobody reading will deadlock.

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(read:, write:) ⇒ Wire

Returns a new instance of Wire.

Parameters:

  • read (IO)

    where the client's frames arrive.

  • write (IO)

    where this side's frames go.



71
72
73
74
# File 'lib/pikuri/lsp/testing.rb', line 71

def initialize(read:, write:)
  @read = read
  @write = write
end

Class Method Details

.channelArray(Hash{Symbol => IO}, Wire)

Returns the client's {stdin:, stdout:} kwargs, and the server side of the same pair.

Returns:

  • (Array(Hash{Symbol => IO}, Wire))

    the client's {stdin:, stdout:} kwargs, and the server side of the same pair.



62
63
64
65
66
67
# File 'lib/pikuri/lsp/testing.rb', line 62

def self.channel
  to_server = IO.pipe
  to_client = IO.pipe
  [{ stdin: to_server[1], stdout: to_client[0] },
   new(read: to_server[0], write: to_client[1])]
end

Instance Method Details

#closevoid

This method returns an undefined value.

Close both ends — from the client's side, indistinguishable from the server dying.



167
168
169
# File 'lib/pikuri/lsp/testing.rb', line 167

def close
  [@write, @read].each { |io| io.close unless io.closed? }
end

#handshake(capabilities: {}, server_info: nil) ⇒ Hash

Play the server half of the handshake: answer initialize, absorb initialized.

Parameters:

  • capabilities (Hash{String => Object}) (defaults to: {})

    what to advertise.

  • server_info (Hash{String => String}, nil) (defaults to: nil)

Returns:

  • (Hash)

    the client's initialize request, so an example can assert on what it declared.

Raises:

  • (RuntimeError)

    if the client said anything else first.



150
151
152
153
154
155
156
157
158
159
160
161
# File 'lib/pikuri/lsp/testing.rb', line 150

def handshake(capabilities: {}, server_info: nil)
  request = take
  raise "expected initialize, got #{request['method'].inspect}" unless request['method'] == 'initialize'

  result = { 'capabilities' => capabilities }
  result['serverInfo'] = server_info if server_info
  reply(request, result)
  absorbed = take
  raise "expected initialized, got #{absorbed['method'].inspect}" unless absorbed['method'] == 'initialized'

  request
end

#notify(method, params = {}) ⇒ void

This method returns an undefined value.

Parameters:

  • method (String)

    e.g. "$/progress".

  • params (Hash) (defaults to: {})


126
127
128
# File 'lib/pikuri/lsp/testing.rb', line 126

def notify(method, params = {})
  send_message('jsonrpc' => '2.0', 'method' => method, 'params' => params)
end

#reply(request, result) ⇒ void

This method returns an undefined value.

Parameters:

  • request (Hash)

    the message being answered; only its id is read.

  • result (Hash, Array, String, Numeric, true, false, nil)

    the result member — nil and [] are real answers.



109
110
111
# File 'lib/pikuri/lsp/testing.rb', line 109

def reply(request, result)
  send_message('jsonrpc' => '2.0', 'id' => request['id'], 'result' => result)
end

#reply_error(request, code:, message:) ⇒ void

This method returns an undefined value.

Parameters:

  • request (Hash)

    the message being refused.

  • code (Integer)

    JSON-RPC code, e.g. -32601 for Method not found.

  • message (String)


118
119
120
121
# File 'lib/pikuri/lsp/testing.rb', line 118

def reply_error(request, code:, message:)
  send_message('jsonrpc' => '2.0', 'id' => request['id'],
               'error' => { 'code' => code, 'message' => message })
end

#request(method, params = {}, id: 'server-1') ⇒ void

This method returns an undefined value.

Ask the client something, which a real server does for window/workDoneProgress/create and workspace/configuration.

Parameters:

  • method (String)
  • params (Hash) (defaults to: {})
  • id (String, Integer) (defaults to: 'server-1')

    ids are the sender's to choose, and a String keeps a server-initiated request distinguishable in a spec.



138
139
140
# File 'lib/pikuri/lsp/testing.rb', line 138

def request(method, params = {}, id: 'server-1')
  send_message('jsonrpc' => '2.0', 'id' => id, 'method' => method, 'params' => params)
end

#send_message(message) ⇒ void

This method returns an undefined value.

Parameters:

  • message (Hash)

    serialized with JSON.generate.



101
102
103
# File 'lib/pikuri/lsp/testing.rb', line 101

def send_message(message)
  send_raw(JSON.generate(message))
end

#send_raw(body) ⇒ void

This method returns an undefined value.

Frame and send body exactly as given — the door recorded bytes come through.

Parameters:

  • body (String)

    a complete JSON-RPC message.



94
95
96
97
# File 'lib/pikuri/lsp/testing.rb', line 94

def send_raw(body)
  @write.write("Content-Length: #{body.bytesize}\r\n\r\n#{body}")
  @write.flush
end

#takeHash{String => Object}

The next message the client wrote, blocking until there is one.

Returns:

  • (Hash{String => Object})

Raises:

  • (EOFError)

    when the client closed its end.



80
81
82
83
84
85
86
87
# File 'lib/pikuri/lsp/testing.rb', line 80

def take
  headers = {}
  while (line = @read.readline("\r\n").chomp("\r\n")) != ''
    key, value = line.split(':', 2)
    headers[key.strip.downcase] = value.strip
  end
  JSON.parse(@read.read(Integer(headers.fetch('content-length'))))
end