mirror of
https://github.com/ruby/ruby.git
synced 2025-09-15 16:44:01 +02:00

This commit is the initial sync of all files from ruby/yarp into ruby/ruby. Notably, it does the following: * Sync all ruby/yarp/lib/ files to ruby/ruby/lib/yarp * Sync all ruby/yarp/src/ files to ruby/ruby/yarp/ * Sync all ruby/yarp/test/ files to ruby/ruby/test/yarp
363 lines
9 KiB
Ruby
363 lines
9 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require_relative "yarp_test_helper"
|
|
require "yarp/language_server"
|
|
|
|
module YARP
|
|
class LanguageServerTest < Test::Unit::TestCase
|
|
module Request
|
|
# Represents a hash pattern.
|
|
class Shape
|
|
attr_reader :values
|
|
|
|
def initialize(values)
|
|
@values = values
|
|
end
|
|
|
|
def ===(other)
|
|
values.all? do |key, value|
|
|
value == :any ? other.key?(key) : value === other[key]
|
|
end
|
|
end
|
|
end
|
|
|
|
# Represents an array pattern.
|
|
class Tuple
|
|
attr_reader :values
|
|
|
|
def initialize(values)
|
|
@values = values
|
|
end
|
|
|
|
def ===(other)
|
|
values.each_with_index.all? { |value, index| value === other[index] }
|
|
end
|
|
end
|
|
|
|
def self.[](value)
|
|
case value
|
|
when Array
|
|
Tuple.new(value.map { |child| self[child] })
|
|
when Hash
|
|
Shape.new(value.transform_values { |child| self[child] })
|
|
else
|
|
value
|
|
end
|
|
end
|
|
end
|
|
|
|
class Initialize < Struct.new(:id)
|
|
def to_hash
|
|
{ method: "initialize", id: id }
|
|
end
|
|
end
|
|
|
|
class Shutdown < Struct.new(:id)
|
|
def to_hash
|
|
{ method: "shutdown", id: id }
|
|
end
|
|
end
|
|
|
|
class TextDocumentDidOpen < Struct.new(:uri, :text)
|
|
def to_hash
|
|
{
|
|
method: "textDocument/didOpen",
|
|
params: { textDocument: { uri: uri, text: text } }
|
|
}
|
|
end
|
|
end
|
|
|
|
class TextDocumentDidChange < Struct.new(:uri, :text)
|
|
def to_hash
|
|
{
|
|
method: "textDocument/didChange",
|
|
params: {
|
|
textDocument: { uri: uri },
|
|
contentChanges: [{ text: text }]
|
|
}
|
|
}
|
|
end
|
|
end
|
|
|
|
class TextDocumentDidClose < Struct.new(:uri)
|
|
def to_hash
|
|
{
|
|
method: "textDocument/didClose",
|
|
params: { textDocument: { uri: uri } }
|
|
}
|
|
end
|
|
end
|
|
|
|
class TextDocumentCodeAction < Struct.new(:id, :uri, :diagnostics)
|
|
def to_hash
|
|
{
|
|
method: "textDocument/codeAction",
|
|
id: id,
|
|
params: {
|
|
textDocument: { uri: uri },
|
|
context: {
|
|
diagnostics: diagnostics,
|
|
},
|
|
},
|
|
}
|
|
end
|
|
end
|
|
|
|
class TextDocumentDiagnostic < Struct.new(:id, :uri)
|
|
def to_hash
|
|
{
|
|
method: "textDocument/diagnostic",
|
|
id: id,
|
|
params: {
|
|
textDocument: { uri: uri },
|
|
}
|
|
}
|
|
end
|
|
end
|
|
|
|
def test_reading_file
|
|
Tempfile.create(%w[test- .rb]) do |file|
|
|
file.write("class Foo; end")
|
|
file.rewind
|
|
|
|
responses = run_server([
|
|
Initialize.new(1),
|
|
Shutdown.new(3)
|
|
])
|
|
|
|
shape = Request[[
|
|
{ id: 1, result: { capabilities: Hash } },
|
|
{ id: 3, result: {} }
|
|
]]
|
|
|
|
assert_operator(shape, :===, responses)
|
|
end
|
|
end
|
|
|
|
def test_clean_shutdown
|
|
responses = run_server([Initialize.new(1), Shutdown.new(2)])
|
|
|
|
shape = Request[[
|
|
{ id: 1, result: { capabilities: Hash } },
|
|
{ id: 2, result: {} }
|
|
]]
|
|
|
|
assert_operator(shape, :===, responses)
|
|
end
|
|
|
|
def test_file_that_does_not_exist
|
|
responses = run_server([
|
|
Initialize.new(1),
|
|
Shutdown.new(3)
|
|
])
|
|
|
|
shape = Request[[
|
|
{ id: 1, result: { capabilities: Hash } },
|
|
{ id: 3, result: {} }
|
|
]]
|
|
|
|
assert_operator(shape, :===, responses)
|
|
end
|
|
|
|
def test_code_action_request
|
|
message = "this is an error"
|
|
diagnostic = {
|
|
range: { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } },
|
|
message: message,
|
|
severity: 1,
|
|
}
|
|
responses = run_server([
|
|
Initialize.new(1),
|
|
TextDocumentDidOpen.new("file:///path/to/file.rb", <<~RUBY),
|
|
1 + (
|
|
RUBY
|
|
TextDocumentCodeAction.new(2, "file:///path/to/file.rb", [diagnostic]),
|
|
Shutdown.new(3)
|
|
])
|
|
|
|
shape = Request[[
|
|
{ id: 1, result: { capabilities: Hash } },
|
|
{ id: 2, result: [
|
|
{
|
|
title: "Report incorrect error: `#{message}`",
|
|
kind: "quickfix",
|
|
diagnostics: [diagnostic],
|
|
command: {
|
|
title: "Report incorrect error",
|
|
command: "vscode.open",
|
|
arguments: [String]
|
|
}
|
|
}
|
|
],
|
|
},
|
|
{ id: 3, result: {} }
|
|
]]
|
|
|
|
assert_operator(shape, :===, responses)
|
|
assert(responses.dig(1, :result, 0, :command, :arguments, 0).include?(URI.encode_www_form_component(message)))
|
|
end
|
|
|
|
def test_code_action_request_no_diagnostic
|
|
responses = run_server([
|
|
Initialize.new(1),
|
|
TextDocumentDidOpen.new("file:///path/to/file.rb", <<~RUBY),
|
|
1 + (
|
|
RUBY
|
|
TextDocumentCodeAction.new(2, "file:///path/to/file.rb", []),
|
|
Shutdown.new(3)
|
|
])
|
|
|
|
shape = Request[[
|
|
{ id: 1, result: { capabilities: Hash } },
|
|
{ id: 2, result: [] },
|
|
{ id: 3, result: {} }
|
|
]]
|
|
|
|
assert_operator(shape, :===, responses)
|
|
end
|
|
|
|
def test_code_action_request_no_content
|
|
message = "this is an error"
|
|
diagnostic = {
|
|
range: { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } },
|
|
message: message,
|
|
severity: 1,
|
|
}
|
|
responses = run_server([
|
|
Initialize.new(1),
|
|
TextDocumentCodeAction.new(2, "file:///path/to/file.rb", [diagnostic]),
|
|
Shutdown.new(3)
|
|
])
|
|
|
|
shape = Request[[
|
|
{ id: 1, result: { capabilities: Hash } },
|
|
{ id: 2, result: nil },
|
|
{ id: 3, result: {} }
|
|
]]
|
|
|
|
assert_operator(shape, :===, responses)
|
|
end
|
|
|
|
def test_diagnostics_request_error
|
|
responses = run_server([
|
|
Initialize.new(1),
|
|
TextDocumentDidOpen.new("file:///path/to/file.rb", <<~RUBY),
|
|
1 + (
|
|
RUBY
|
|
TextDocumentDiagnostic.new(2, "file:///path/to/file.rb"),
|
|
Shutdown.new(3)
|
|
])
|
|
|
|
shape = Request[[
|
|
{ id: 1, result: { capabilities: Hash } },
|
|
{ id: 2, result: { kind: "full", items: [
|
|
{
|
|
range: {
|
|
start: { line: Integer, character: Integer },
|
|
end: { line: Integer, character: Integer }
|
|
},
|
|
message: String,
|
|
severity: Integer
|
|
},
|
|
] } },
|
|
{ id: 3, result: {} }
|
|
]]
|
|
|
|
assert_operator(shape, :===, responses)
|
|
assert(responses.dig(1, :result, :items).count { |item| item[:severity] == 1 } > 0)
|
|
end
|
|
|
|
def test_diagnostics_request_warning
|
|
responses = run_server([
|
|
Initialize.new(1),
|
|
TextDocumentDidOpen.new("file:///path/to/file.rb", <<~RUBY),
|
|
a/b /c
|
|
RUBY
|
|
TextDocumentDiagnostic.new(2, "file:///path/to/file.rb"),
|
|
Shutdown.new(3)
|
|
])
|
|
|
|
shape = Request[[
|
|
{ id: 1, result: { capabilities: Hash } },
|
|
{ id: 2, result: { kind: "full", items: [
|
|
{
|
|
range: {
|
|
start: { line: Integer, character: Integer },
|
|
end: { line: Integer, character: Integer }
|
|
},
|
|
message: String,
|
|
severity: Integer
|
|
},
|
|
] } },
|
|
{ id: 3, result: {} }
|
|
]]
|
|
|
|
assert_operator(shape, :===, responses)
|
|
assert(responses.dig(1, :result, :items).count { |item| item[:severity] == 2 } > 0)
|
|
end
|
|
|
|
def test_diagnostics_request_nothing
|
|
responses = run_server([
|
|
Initialize.new(1),
|
|
TextDocumentDidOpen.new("file:///path/to/file.rb", <<~RUBY),
|
|
a = 1
|
|
RUBY
|
|
TextDocumentDiagnostic.new(2, "file:///path/to/file.rb"),
|
|
Shutdown.new(3)
|
|
])
|
|
|
|
shape = Request[[
|
|
{ id: 1, result: { capabilities: Hash } },
|
|
{ id: 2, result: { kind: "full", items: [] } },
|
|
{ id: 3, result: {} }
|
|
]]
|
|
|
|
assert_operator(shape, :===, responses)
|
|
assert_equal(0, responses.dig(1, :result, :items).size)
|
|
end
|
|
|
|
def test_diagnostics_request_no_content
|
|
responses = run_server([
|
|
Initialize.new(1),
|
|
TextDocumentDiagnostic.new(2, "file:///path/to/file.rb"),
|
|
Shutdown.new(3)
|
|
])
|
|
|
|
shape = Request[[
|
|
{ id: 1, result: { capabilities: Hash } },
|
|
{ id: 2, result: nil },
|
|
{ id: 3, result: {} }
|
|
]]
|
|
|
|
assert_operator(shape, :===, responses)
|
|
end
|
|
|
|
private
|
|
|
|
def write(content)
|
|
request = content.to_hash.merge(jsonrpc: "2.0").to_json
|
|
"Content-Length: #{request.bytesize}\r\n\r\n#{request}"
|
|
end
|
|
|
|
def read(content)
|
|
[].tap do |messages|
|
|
while (headers = content.gets("\r\n\r\n"))
|
|
source = content.read(headers[/Content-Length: (\d+)/i, 1].to_i)
|
|
messages << JSON.parse(source, symbolize_names: true)
|
|
end
|
|
end
|
|
end
|
|
|
|
def run_server(messages)
|
|
input = StringIO.new(messages.map { |message| write(message) }.join)
|
|
output = StringIO.new
|
|
|
|
LanguageServer.new(
|
|
input: input,
|
|
output: output,
|
|
).run
|
|
|
|
read(output.tap(&:rewind))
|
|
end
|
|
end
|
|
end
|