Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Use index to detect test library #3031

Draft
wants to merge 27 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 0 additions & 27 deletions lib/ruby_lsp/global_state.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,6 @@ module RubyLsp
class GlobalState
extend T::Sig

sig { returns(String) }
attr_reader :test_library

sig { returns(String) }
attr_accessor :formatter

Expand Down Expand Up @@ -39,7 +36,6 @@ def initialize

@formatter = T.let("auto", String)
@linters = T.let([], T::Array[String])
@test_library = T.let("minitest", String)
@has_type_checker = T.let(true, T::Boolean)
@index = T.let(RubyIndexer::Index.new, RubyIndexer::Index)
@supported_formatters = T.let({}, T::Hash[String, Requests::Support::Formatter])
Expand Down Expand Up @@ -117,9 +113,6 @@ def apply_options(options)
Notification.window_log_message("Auto detected linters: #{@linters.join(", ")}")
end

@test_library = detect_test_library(direct_dependencies)
notifications << Notification.window_log_message("Detected test library: #{@test_library}")

@has_type_checker = detect_typechecker(all_dependencies)
if @has_type_checker
notifications << Notification.window_log_message(
Expand Down Expand Up @@ -209,26 +202,6 @@ def detect_linters(dependencies, all_dependencies)
linters
end

sig { params(dependencies: T::Array[String]).returns(String) }
def detect_test_library(dependencies)
if dependencies.any?(/^rspec/)
"rspec"
# A Rails app may have a dependency on minitest, but we would instead want to use the Rails test runner provided
# by ruby-lsp-rails. A Rails app doesn't need to depend on the rails gem itself, individual components like
# activestorage may be added to the gemfile so that other components aren't downloaded. Check for the presence
# of bin/rails to support these cases.
elsif bin_rails_present
"rails"
# NOTE: Intentionally ends with $ to avoid mis-matching minitest-reporters, etc. in a Rails app.
elsif dependencies.any?(/^minitest$/)
"minitest"
elsif dependencies.any?(/^test-unit/)
"test-unit"
else
"unknown"
end
end

sig { params(dependencies: T::Array[String]).returns(T::Boolean) }
def detect_typechecker(dependencies)
return false if ENV["RUBY_LSP_BYPASS_TYPECHECKER"]
Expand Down
10 changes: 5 additions & 5 deletions lib/ruby_lsp/listeners/code_lens.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,14 @@ class CodeLens
sig do
params(
response_builder: ResponseBuilders::CollectionResponseBuilder[Interface::CodeLens],
global_state: GlobalState,
test_library: String,
uri: URI::Generic,
dispatcher: Prism::Dispatcher,
).void
end
def initialize(response_builder, global_state, uri, dispatcher)
def initialize(response_builder, test_library, uri, dispatcher)
@response_builder = response_builder
@global_state = global_state
@test_library = test_library
@uri = T.let(uri, URI::Generic)
@path = T.let(uri.to_standardized_path, T.nilable(String))
# visibility_stack is a stack of [current_visibility, previous_visibility]
Expand Down Expand Up @@ -178,7 +178,7 @@ def on_call_node_leave(node)
sig { params(node: Prism::Node, name: String, command: String, kind: Symbol, id: String).void }
def add_test_code_lens(node, name:, command:, kind:, id: name)
# don't add code lenses if the test library is not supported or unknown
return unless SUPPORTED_TEST_LIBRARIES.include?(@global_state.test_library) && @path
return unless SUPPORTED_TEST_LIBRARIES.include?(@test_library) && @path

arguments = [
@path,
Expand Down Expand Up @@ -235,7 +235,7 @@ def generate_test_command(group_stack: [], spec_name: nil, method_name: nil)
command += " -Ispec" if File.fnmatch?("**/spec/**/*", path, File::FNM_PATHNAME)
command += " #{path}"

case @global_state.test_library
case @test_library
when "minitest"
last_dynamic_reference_index = group_stack.rindex(DYNAMIC_REFERENCE_MARKER)
command += if last_dynamic_reference_index
Expand Down
6 changes: 3 additions & 3 deletions lib/ruby_lsp/requests/code_lens.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,18 @@ def provider

sig do
params(
global_state: GlobalState,
uri: URI::Generic,
test_library: String,
dispatcher: Prism::Dispatcher,
).void
end
def initialize(global_state, uri, dispatcher)
def initialize(uri, test_library, dispatcher)
@response_builder = T.let(
ResponseBuilders::CollectionResponseBuilder[Interface::CodeLens].new,
ResponseBuilders::CollectionResponseBuilder[Interface::CodeLens],
)
super()
Listeners::CodeLens.new(@response_builder, global_state, uri, dispatcher)
Listeners::CodeLens.new(@response_builder, test_library, uri, dispatcher)

Addon.addons.each do |addon|
addon.create_code_lens_listener(@response_builder, uri, dispatcher)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@st0012 @vinistock I'm wondering how we should update this since currently ruby-lsp-rails checks @global_state.test_library == "rails". What if we make test_library be an optional keyword argument? That would prevent breaking the API for other addons.

Copy link
Contributor Author

@andyw8 andyw8 Jan 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternatively, we change it to pass document instead of uri (since uri is available via document).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be considered a breaking change anyway so I think changing args is expected?

Copy link
Contributor Author

@andyw8 andyw8 Jan 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True. I've updated it to pass document insead of test_library, which should allow for more flexibility in future. I'm still considering if uri could be dropped.

(also have to investigate some test failures).

Expand Down
22 changes: 22 additions & 0 deletions lib/ruby_lsp/ruby_document.rb
Original file line number Diff line number Diff line change
Expand Up @@ -240,5 +240,27 @@ def locate_node(position, node_types: [])
node_types: node_types,
)
end

sig { returns(String) }
def test_library
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Part of the advantage of looking at the ancestors is that we become more accurate even in very uncommon setups, like mixing test frameworks - which is something that came up a few times.

Considering that test files can have multiple test classes and they can inherit from different parents, is this approach flexible enough to account for that?

What was the idea behind putting it in document versus doing it in the code lens implementation?

# TODO: return 'none' immediately if path doesn't contain 'test' or 'spec' ?
class_entries = @global_state.index.entries_for(@uri.to_s, RubyIndexer::Entry::Class)
return "none" unless class_entries

# TODO: consider performance hit
ancestors = class_entries
.map { @global_state.index.linearized_ancestors_of(_1.name) }.flatten

# ActiveSupport::TestCase is a subclass of Minitest::Test so we must check for it first
if ancestors.include?("ActiveSupport::TestCase")
"rails"
elsif ancestors.include?("Minitest::Test")
"minitest"
elsif ancestors.include?("Test::Unit::TestCase")
"test-unit"
else
"none"
end
end
end
end
4 changes: 2 additions & 2 deletions lib/ruby_lsp/server.rb
Original file line number Diff line number Diff line change
Expand Up @@ -461,7 +461,7 @@ def run_combined_requests(message)
folding_range = Requests::FoldingRanges.new(parse_result.comments, dispatcher)
document_symbol = Requests::DocumentSymbol.new(uri, dispatcher)
document_link = Requests::DocumentLink.new(uri, parse_result.comments, dispatcher)
code_lens = Requests::CodeLens.new(@global_state, uri, dispatcher)
code_lens = Requests::CodeLens.new(uri, document.test_library, dispatcher) if document.is_a?(RubyDocument)
inlay_hint = Requests::InlayHints.new(document, T.must(@store.features_configuration.dig(:inlayHint)), dispatcher)

# Re-index the file as it is modified. This mode of indexing updates entries only. Require path trees are only
Expand All @@ -477,7 +477,7 @@ def run_combined_requests(message)
document.cache_set("textDocument/foldingRange", folding_range.perform)
document.cache_set("textDocument/documentSymbol", document_symbol.perform)
document.cache_set("textDocument/documentLink", document_link.perform)
document.cache_set("textDocument/codeLens", code_lens.perform)
document.cache_set("textDocument/codeLens", code_lens.perform) if code_lens
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if is temporary

document.cache_set("textDocument/inlayHint", inlay_hint.perform)

send_message(Result.new(id: message[:id], response: document.cache_get(message[:method])))
Expand Down
49 changes: 0 additions & 49 deletions test/global_state_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,55 +5,6 @@

module RubyLsp
class GlobalStateTest < Minitest::Test
def test_detects_no_test_library_when_there_are_no_dependencies
stub_direct_dependencies({})

state = GlobalState.new
state.apply_options({})
assert_equal("unknown", state.test_library)
end

def test_detects_minitest
stub_direct_dependencies("minitest" => "1.2.3")

state = GlobalState.new
state.apply_options({})
assert_equal("minitest", state.test_library)
end

def test_does_not_detect_minitest_related_gems_as_minitest
stub_direct_dependencies("minitest-reporters" => "1.2.3")

state = GlobalState.new
state.apply_options({})
assert_equal("unknown", state.test_library)
end

def test_detects_test_unit
stub_direct_dependencies("test-unit" => "1.2.3")

state = GlobalState.new
state.apply_options({})
assert_equal("test-unit", state.test_library)
end

def test_detects_rails_if_minitest_is_present_and_bin_rails_exists
stub_direct_dependencies("minitest" => "1.2.3")

state = GlobalState.new
state.stubs(:bin_rails_present).returns(true)
state.apply_options({})
assert_equal("rails", state.test_library)
end

def test_detects_rspec_if_both_rails_and_rspec_are_present
stub_direct_dependencies("rspec" => "1.2.3")

state = GlobalState.new
state.apply_options({})
assert_equal("rspec", state.test_library)
end

def test_apply_option_selects_formatter
state = GlobalState.new
state.apply_options({ initializationOptions: { formatter: "syntax_tree" } })
Expand Down
40 changes: 18 additions & 22 deletions test/requests/code_lens_expectations_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,24 @@
class CodeLensExpectationsTest < ExpectationsTestRunner
expectations_tests RubyLsp::Requests::CodeLens, "code_lens"

def setup
super
@test_library = "minitest"
end

def run_expectations(source)
uri = URI("file://#{@_path}")
document = RubyLsp::RubyDocument.new(source: source, version: 1, uri: uri, global_state: @global_state)

dispatcher = Prism::Dispatcher.new
stub_test_library("minitest")
listener = RubyLsp::Requests::CodeLens.new(@global_state, uri, dispatcher)
# stub_test_library("minitest")
listener = RubyLsp::Requests::CodeLens.new(uri, @test_library, dispatcher)
dispatcher.dispatch(document.parse_result.value)
listener.perform
end

def test_command_generation_for_minitest
stub_test_library("minitest")
# stub_test_library("minitest")
source = <<~RUBY
class FooTest < MiniTest::Test
def test_bar; end
Expand All @@ -30,7 +35,7 @@ def test_bar; end
document = RubyLsp::RubyDocument.new(source: source, version: 1, uri: uri, global_state: @global_state)

dispatcher = Prism::Dispatcher.new
listener = RubyLsp::Requests::CodeLens.new(@global_state, uri, dispatcher)
listener = RubyLsp::Requests::CodeLens.new(uri, @test_library, dispatcher)
dispatcher.dispatch(document.parse_result.value)
response = listener.perform

Expand All @@ -49,7 +54,6 @@ def test_bar; end
end

def test_command_generation_for_minitest_spec
stub_test_library("minitest")
source = <<~RUBY
class FooTest < MiniTest::Test
describe "a" do
Expand All @@ -62,7 +66,7 @@ class FooTest < MiniTest::Test
document = RubyLsp::RubyDocument.new(source: source, version: 1, uri: uri, global_state: @global_state)

dispatcher = Prism::Dispatcher.new
listener = RubyLsp::Requests::CodeLens.new(@global_state, uri, dispatcher)
listener = RubyLsp::Requests::CodeLens.new(uri, @test_library, dispatcher)
dispatcher.dispatch(document.parse_result.value)
response = listener.perform

Expand All @@ -86,7 +90,7 @@ class FooTest < MiniTest::Test
end

def test_command_generation_for_test_unit
stub_test_library("test-unit")
@test_library = "test-unit"
source = <<~RUBY
class FooTest < Test::Unit::TestCase
def test_bar; end
Expand All @@ -97,7 +101,7 @@ def test_bar; end
document = RubyLsp::RubyDocument.new(source: source, version: 1, uri: uri, global_state: @global_state)

dispatcher = Prism::Dispatcher.new
listener = RubyLsp::Requests::CodeLens.new(@global_state, uri, dispatcher)
listener = RubyLsp::Requests::CodeLens.new(uri, @test_library, dispatcher)
dispatcher.dispatch(document.parse_result.value)
response = listener.perform

Expand All @@ -123,8 +127,7 @@ def test_bar; end
document = RubyLsp::RubyDocument.new(source: source, version: 1, uri: uri, global_state: @global_state)

dispatcher = Prism::Dispatcher.new
stub_test_library("unknown")
listener = RubyLsp::Requests::CodeLens.new(@global_state, uri, dispatcher)
listener = RubyLsp::Requests::CodeLens.new(uri, "unknown", dispatcher)
dispatcher.dispatch(document.parse_result.value)
response = listener.perform

Expand All @@ -142,8 +145,7 @@ def test_bar; end
document = RubyLsp::RubyDocument.new(source: source, version: 1, uri: uri, global_state: @global_state)

dispatcher = Prism::Dispatcher.new
stub_test_library("rspec")
listener = RubyLsp::Requests::CodeLens.new(@global_state, uri, dispatcher)
listener = RubyLsp::Requests::CodeLens.new(uri, "rspec", dispatcher)
dispatcher.dispatch(document.parse_result.value)
response = listener.perform

Expand All @@ -161,8 +163,7 @@ def test_bar; end
document = RubyLsp::RubyDocument.new(source: source, version: 1, uri: uri, global_state: @global_state)

dispatcher = Prism::Dispatcher.new
stub_test_library("minitest")
listener = RubyLsp::Requests::CodeLens.new(@global_state, uri, dispatcher)
listener = RubyLsp::Requests::CodeLens.new(uri, @test_library, dispatcher)
dispatcher.dispatch(document.parse_result.value)
response = listener.perform

Expand All @@ -180,8 +181,7 @@ def test_no_code_lens_for_unsaved_specs
document = RubyLsp::RubyDocument.new(source: source, version: 1, uri: uri, global_state: @global_state)

dispatcher = Prism::Dispatcher.new
stub_test_library("minitest")
listener = RubyLsp::Requests::CodeLens.new(@global_state, uri, dispatcher)
listener = RubyLsp::Requests::CodeLens.new(uri, @test_library, dispatcher)
dispatcher.dispatch(document.parse_result.value)
response = listener.perform

Expand All @@ -190,6 +190,7 @@ def test_no_code_lens_for_unsaved_specs

def test_code_lens_addons
source = <<~RUBY
class Minitest::Test; end # TODO need?
class Test < Minitest::Test; end
RUBY

Expand Down Expand Up @@ -220,7 +221,6 @@ class Test < Minitest::Test; end
end

def test_no_code_lens_for_nested_defs
stub_test_library("test-unit")
source = <<~RUBY
class FooTest < Test::Unit::TestCase
def test_bar
Expand All @@ -233,7 +233,7 @@ def test_baz; end
document = RubyLsp::RubyDocument.new(source: source, version: 1, uri: uri, global_state: @global_state)

dispatcher = Prism::Dispatcher.new
listener = RubyLsp::Requests::CodeLens.new(@global_state, uri, dispatcher)
listener = RubyLsp::Requests::CodeLens.new(uri, "test-unit", dispatcher)
dispatcher.dispatch(document.parse_result.value)
response = listener.perform

Expand Down Expand Up @@ -283,8 +283,4 @@ def version
end
end
end

def stub_test_library(name)
@global_state.stubs(:test_library).returns(name)
end
end
Loading
Loading