Class: Pikuri::Lsp::Testing::FakeServer

Inherits:
Object
  • Object
show all
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 { |message| message['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

Instance Method Summary collapse

Constructor Details

#initialize(capabilities: {}, server_info: nil, initialize_result: nil) ⇒ FakeServer

Returns a new instance of FakeServer.

Parameters:

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

    advertised at initialize; the client's ClientWrapper#supports? reads exactly this.

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

    name / version.

  • initialize_result (String, nil) (defaults to: nil)

    a recorded initialize result member, as JSON text, put on the wire byte-for-byte. Overrides capabilities and server_info when given.



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

#channelHash{Symbol => IO} (readonly)

Returns the {stdin:, stdout:} kwargs for Connection#initialize or ClientWrapper#initialize.

Returns:



200
201
202
# File 'lib/pikuri/lsp/testing.rb', line 200

def channel
  @channel
end

#wireWire (readonly)

Returns the raw channel, for a sequence no route can express.

Returns:

  • (Wire)

    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 { |message| message['id'] == 'srv-1' && message.key?('result') }

Parameters:

  • method (String, nil) (defaults to: nil)

    match on the method member; omit and pass a block to match on anything else (a reply to a server-initiated request carries no method at all).

  • timeout (Numeric) (defaults to: 2)

    seconds before giving up.

Yield Parameters:

  • message (Hash)

Yield Returns:

  • (Boolean)

Returns:

  • (Hash)

    the first matching message.

Raises:

  • (RuntimeError)

    on timeout, listing what did arrive — a spec waiting for the wrong thing must fail, not hang.



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? ? ->(message) { yield(message) } : ->(message) { message['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 { |message| message['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.

Parameters:

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


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.

Parameters:

  • method (String)

    e.g. "textDocument/definition".

Yield Parameters:

  • params (Hash, nil)

    the request's params.

Yield Returns:

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

    the result member — nil is an answer, not a miss.



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.

Parameters:

  • method (String)

Yield Parameters:

  • params (Hash, nil)

Yield Returns:

  • (String)

    JSON text.



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

#receivedArray<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.

Returns:

  • (Array<Hash>)

    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.

Parameters:

  • method (String)
  • code (Integer) (defaults to: METHOD_NOT_FOUND)
  • message (String) (defaults to: nil)


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, message || "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.

Parameters:

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


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.

Parameters:

  • body (String)

    a complete JSON-RPC message as JSON text.



279
280
281
# File 'lib/pikuri/lsp/testing.rb', line 279

def send_frame(body)
  @wire.send_raw(body)
end

#serveself

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.

Returns:

  • (self)


260
261
262
263
# File 'lib/pikuri/lsp/testing.rb', line 260

def serve
  @thread ||= Thread.new { serve_loop }
  self
end

#stopvoid

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