Class: Pikuri::Lsp::Testing::FakeServer
- Inherits:
-
Object
- Object
- Pikuri::Lsp::Testing::FakeServer
- Defined in:
- lib/pikuri/lsp/testing.rb
Overview
A language server that answers on its own thread: the handshake for free, one block per method, and everything the client said kept for assertions.
server = FakeServer.new(capabilities: { 'definitionProvider' => true })
server.on('textDocument/definition') { |params| [{ 'uri' => params['uri'], 'range' => nil }] }
server.on_json('typeHierarchy/supertypes') { File.read('spec/fixtures/ruby_lsp_supertypes.json') }
server.serve
client.start # … and whatever else the example drives
server.await('initialized') # the last frame of the handshake
server.received.map { || ['method'] }
# => ["initialize", "initialized"]
server.stop
An unrouted request is answered -32601 Method not found, which is what
ruby-lsp does for an operation it never advertised — so a call the example
did not expect fails loudly instead of hanging.
Thread-safe for the calls above: the serving thread reads and answers while the example scripts routes and reads #received.
Defined Under Namespace
Classes: Refusal
Constant Summary collapse
- METHOD_NOT_FOUND =
JSON-RPC's code for a method the server does not implement.
-32_601
Instance Attribute Summary collapse
-
#channel ⇒ Hash{Symbol => IO}
readonly
The
{stdin:, stdout:}kwargs for Connection#initialize or ClientWrapper#initialize. -
#wire ⇒ Wire
readonly
The raw channel, for a sequence no route can express.
Instance Method Summary collapse
-
#await(method = nil, timeout: 2) {|message| ... } ⇒ Hash
Block until the client has sent a matching message, and return it.
-
#crash! ⇒ void
Die: close the channel with no warning and no
exit, the way a crashed child does. -
#initialize(capabilities: {}, server_info: nil, initialize_result: nil) ⇒ FakeServer
constructor
A new instance of FakeServer.
-
#notify(method, params = {}) ⇒ void
Push a notification at the client, unprompted.
-
#on(method) {|params| ... } ⇒ void
Answer
methodwith whatever the block returns, serialized as theresultmember. -
#on_json(method) {|params| ... } ⇒ void
Answer
methodwith recorded bytes: the block returns theresultmember as JSON text, spliced into the response with only the id filled in, so key order and spacing survive exactly as captured. -
#received ⇒ Array<Hash>
Every message the client sent, in order, parsed.
-
#refuse(method, code: METHOD_NOT_FOUND, message: nil) ⇒ void
Refuse
methodthe way a server refuses an operation it does not have. -
#request(method, params = {}, id: 'server-1') ⇒ void
Ask the client something, to exercise its reply to a server-initiated request.
-
#send_frame(body) ⇒ void
Put a complete recorded frame on the wire untouched — how a captured
$/progressorwindow/logMessagenotification is replayed. -
#serve ⇒ self
Start the serving thread; a second call is a no-op.
-
#stop ⇒ void
Stop serving and close the channel.
Constructor Details
#initialize(capabilities: {}, server_info: nil, initialize_result: nil) ⇒ FakeServer
Returns a new instance of FakeServer.
208 209 210 211 212 213 214 215 216 |
# File 'lib/pikuri/lsp/testing.rb', line 208 def initialize(capabilities: {}, server_info: nil, initialize_result: nil) @capabilities = capabilities @server_info = server_info @initialize_result = initialize_result @routes = {} @received = [] @mutex = Mutex.new @channel, @wire = Wire.channel end |
Instance Attribute Details
#channel ⇒ Hash{Symbol => IO} (readonly)
Returns the {stdin:, stdout:} kwargs for
Connection#initialize or ClientWrapper#initialize.
200 201 202 |
# File 'lib/pikuri/lsp/testing.rb', line 200 def channel @channel end |
#wire ⇒ Wire (readonly)
Returns the raw channel, for a sequence no route can express.
196 197 198 |
# File 'lib/pikuri/lsp/testing.rb', line 196 def wire @wire end |
Instance Method Details
#await(method = nil, timeout: 2) {|message| ... } ⇒ Hash
Block until the client has sent a matching message, and return it.
server.await('textDocument/didOpen')
server.await { || ['id'] == 'srv-1' && .key?('result') }
324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 |
# File 'lib/pikuri/lsp/testing.rb', line 324 def await(method = nil, timeout: 2) matcher = block_given? ? ->() { yield() } : ->() { ['method'] == method } deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout loop do found = @mutex.synchronize { @received.find(&matcher) } return found if found if Process.clock_gettime(Process::CLOCK_MONOTONIC) > deadline raise "waited #{timeout}s for #{method || 'a match'}; received " \ "#{received.map { || ['method'] }.inspect}" end sleep 0.01 end end |
#crash! ⇒ void
This method returns an undefined value.
Die: close the channel with no warning and no exit, the way a crashed
child does.
298 299 300 |
# File 'lib/pikuri/lsp/testing.rb', line 298 def crash! @wire.close end |
#notify(method, params = {}) ⇒ void
This method returns an undefined value.
Push a notification at the client, unprompted.
270 271 272 |
# File 'lib/pikuri/lsp/testing.rb', line 270 def notify(method, params = {}) @wire.notify(method, params) end |
#on(method) {|params| ... } ⇒ void
This method returns an undefined value.
Answer method with whatever the block returns, serialized as the
result member.
226 227 228 229 |
# File 'lib/pikuri/lsp/testing.rb', line 226 def on(method, &block) @mutex.synchronize { @routes[method] = { json: false, block: block } } nil end |
#on_json(method) {|params| ... } ⇒ void
This method returns an undefined value.
Answer method with recorded bytes: the block returns the result
member as JSON text, spliced into the response with only the id
filled in, so key order and spacing survive exactly as captured.
239 240 241 242 |
# File 'lib/pikuri/lsp/testing.rb', line 239 def on_json(method, &block) @mutex.synchronize { @routes[method] = { json: true, block: block } } nil end |
#received ⇒ Array<Hash>
Returns every message the client sent, in order, parsed. A snapshot, and it can lag by one message: a client call returns when its bytes are written, and the serving thread records them a moment later. Assert through #await rather than racing it.
306 307 308 |
# File 'lib/pikuri/lsp/testing.rb', line 306 def received @mutex.synchronize { @received.dup } end |
#refuse(method, code: METHOD_NOT_FOUND, message: nil) ⇒ void
This method returns an undefined value.
Refuse method the way a server refuses an operation it does not have.
250 251 252 253 |
# File 'lib/pikuri/lsp/testing.rb', line 250 def refuse(method, code: METHOD_NOT_FOUND, message: nil) on(method) { raise Refusal.new(code, || "Method not found: #{method}") } nil end |
#request(method, params = {}, id: 'server-1') ⇒ void
This method returns an undefined value.
Ask the client something, to exercise its reply to a server-initiated request.
290 291 292 |
# File 'lib/pikuri/lsp/testing.rb', line 290 def request(method, params = {}, id: 'server-1') @wire.request(method, params, id: id) end |
#send_frame(body) ⇒ void
This method returns an undefined value.
Put a complete recorded frame on the wire untouched — how a captured
$/progress or window/logMessage notification is replayed.
279 280 281 |
# File 'lib/pikuri/lsp/testing.rb', line 279 def send_frame(body) @wire.send_raw(body) end |
#serve ⇒ self
Start the serving thread; a second call is a no-op. It has to be running before the client's handshake, which is the first thing that needs an answer.
260 261 262 263 |
# File 'lib/pikuri/lsp/testing.rb', line 260 def serve @thread ||= Thread.new { serve_loop } self end |
#stop ⇒ void
This method returns an undefined value.
Stop serving and close the channel. Idempotent.
343 344 345 346 347 348 |
# File 'lib/pikuri/lsp/testing.rb', line 343 def stop @wire.close @thread&.join(2) @thread&.kill @thread = nil end |