Skip to content

Commit 0966f07

Browse files
committed
Migrate test discovery to use Rubydex
1 parent 9ca38db commit 0966f07

5 files changed

Lines changed: 93 additions & 134 deletions

File tree

lib/ruby_lsp/listeners/spec_style.rb

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ def initialize(response_builder, global_state, dispatcher, uri)
3434
#: (Prism::ClassNode) -> void
3535
def on_class_node_enter(node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod
3636
with_test_ancestor_tracking(node) do |name, ancestors|
37-
@spec_group_id_stack << (ancestors.include?("Minitest::Spec") ? ClassGroup.new(name) : nil)
37+
@spec_group_id_stack << (spec_group?(ancestors, name) ? ClassGroup.new(name) : nil)
3838
end
3939
end
4040

@@ -81,6 +81,11 @@ def on_call_node_leave(node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMet
8181

8282
private
8383

84+
#: (Array[String], String) -> bool
85+
def spec_group?(ancestors, fully_qualified_name)
86+
fully_qualified_name != "Minitest::Spec" && ancestors.include?("Minitest::Spec")
87+
end
88+
8489
#: (Prism::CallNode) -> void
8590
def handle_describe(node)
8691
# Describes will include the nesting of all classes and all outer describes as part of its ID, unlike classes

lib/ruby_lsp/listeners/test_discovery.rb

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ class TestDiscovery
1313
def initialize(response_builder, global_state, uri)
1414
@response_builder = response_builder
1515
@uri = uri
16-
@index = global_state.index #: RubyIndexer::Index
16+
@graph = global_state.graph #: Rubydex::Graph
1717
@visibility_stack = [:public] #: Array[Symbol]
1818
@nesting = [] #: Array[String]
1919
end
@@ -64,22 +64,29 @@ def calc_attached_ancestors(node, fully_qualified_name)
6464
superclass = node.superclass
6565

6666
begin
67-
ancestors = @index.linearized_ancestors_of(fully_qualified_name)
68-
# If the project has no bundle and a test class inherits from `Minitest::Test`, the linearized ancestors will
69-
# not include the parent class because we never indexed it in the first place. Here we add the superclass
70-
# directly, so that we can support running tests in projects without a bundle
71-
return ancestors if ancestors.length > 1
72-
73-
# If all we found is the class itself, then manually include the parent class
74-
if ancestors.first == fully_qualified_name && superclass
75-
return [*ancestors, superclass.slice]
67+
declaration = @graph[fully_qualified_name]
68+
69+
unless declaration.is_a?(Rubydex::Namespace)
70+
# When there are dynamic parts in the constant path, we will not have indexed the namespace. We can still
71+
# provide test functionality if the class inherits directly from Test::Unit::TestCase or Minitest::Test
72+
return [superclass&.slice].compact
73+
end
74+
75+
ancestors = declaration.ancestors.map(&:name)
76+
superclass_ref = declaration.definitions
77+
.filter_map { |d| d.superclass if d.is_a?(Rubydex::ClassDefinition) }
78+
.find { |ref| !ref.is_a?(Rubydex::ResolvedConstantReference) || ref.declaration.name != "Object" }
79+
80+
# If we couldn't resolve the parent class, then artificially inject it into the ancestors
81+
if superclass_ref.is_a?(Rubydex::UnresolvedConstantReference) && superclass
82+
insert_index = ancestors.index(fully_qualified_name) #: as !nil
83+
insert_index += 1
84+
ancestors.insert(insert_index, superclass.slice)
85+
return ancestors
7686
end
7787

88+
# If the parent class is properly resolved or if there isn't one, then just use the ancestors
7889
ancestors
79-
rescue RubyIndexer::Index::NonExistingNamespaceError
80-
# When there are dynamic parts in the constant path, we will not have indexed the namespace. We can still
81-
# provide test functionality if the class inherits directly from Test::Unit::TestCase or Minitest::Test
82-
[superclass&.slice].compact
8390
end
8491
end
8592

lib/ruby_lsp/listeners/test_style.rb

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -174,9 +174,10 @@ def initialize(response_builder, global_state, dispatcher, uri)
174174
#: (Prism::ClassNode node) -> void
175175
def on_class_node_enter(node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod
176176
with_test_ancestor_tracking(node) do |name, ancestors|
177-
@framework = :test_unit if ancestors.include?("Test::Unit::TestCase")
177+
is_test_unit = test_unit_group?(ancestors, name)
178+
@framework = :test_unit if is_test_unit
178179

179-
if @framework == :test_unit || non_declarative_minitest?(ancestors, name)
180+
if is_test_unit || non_declarative_minitest?(ancestors, name)
180181
test_item = Requests::Support::TestItem.new(
181182
name,
182183
name,
@@ -259,17 +260,28 @@ def last_test_group
259260
@parent_stack[index] #: as Requests::Support::TestItem | ResponseBuilders::TestCollection
260261
end
261262

262-
#: (Array[String] attached_ancestors, String fully_qualified_name) -> bool
263+
#: (Array[String], String) -> bool
264+
def test_unit_group?(ancestors, fully_qualified_name)
265+
fully_qualified_name != "Test::Unit::TestCase" && ancestors.include?("Test::Unit::TestCase")
266+
end
267+
268+
#: (Array[String], String) -> bool
263269
def non_declarative_minitest?(attached_ancestors, fully_qualified_name)
270+
return false if ["Minitest::Spec", "Minitest::Test", "ActiveSupport::TestCase"].include?(fully_qualified_name)
264271
return false unless attached_ancestors.include?("Minitest::Test")
265272

266273
# We only support regular Minitest tests. The declarative syntax provided by ActiveSupport is handled by the
267274
# Rails add-on
268-
name_parts = fully_qualified_name.split("::")
269-
singleton_name = "#{name_parts.join("::")}::<#{name_parts.last}>"
270-
!@index.linearized_ancestors_of(singleton_name).include?("ActiveSupport::Testing::Declarative")
271-
rescue RubyIndexer::Index::NonExistingNamespaceError
272-
true
275+
276+
declaration = @graph[fully_qualified_name]
277+
# If we don't find the fully qualified name in the graph, it means there's a dynamic portion in the test class
278+
# definition. In that case, if the ancestors did include `Minitest::Test`, we always assume it's a test
279+
return true unless declaration.is_a?(Rubydex::Namespace)
280+
281+
singleton = declaration.singleton_class
282+
return !singleton.ancestors.map(&:name).include?("ActiveSupport::Testing::Declarative") if singleton
283+
284+
!attached_ancestors.include?("ActiveSupport::TestCase")
273285
end
274286
end
275287
end

lib/ruby_lsp/requests/discover_tests.rb

Lines changed: 5 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -19,55 +19,19 @@ def initialize(global_state, document, dispatcher)
1919
@document = document
2020
@dispatcher = dispatcher
2121
@response_builder = ResponseBuilders::TestCollection.new #: ResponseBuilders::TestCollection
22-
@index = global_state.index #: RubyIndexer::Index
2322
end
2423

2524
# @override
2625
#: -> Array[Support::TestItem]
2726
def perform
28-
uri = @document.uri
27+
Listeners::TestStyle.new(@response_builder, @global_state, @dispatcher, @document.uri)
28+
Listeners::SpecStyle.new(@response_builder, @global_state, @dispatcher, @document.uri)
2929

30-
# We normally only index test files once they are opened in the editor to save memory and avoid doing
31-
# unnecessary work. If the file is already opened and we already indexed it, then we can just discover the tests
32-
# straight away.
33-
#
34-
# However, if the user navigates to a specific test file from the explorer with nothing opened in the UI, then
35-
# we will not have indexed the test file yet and trying to linearize the ancestor of the class will fail. In
36-
# this case, we have to instantiate the indexer listener first, so that we insert classes, modules and methods
37-
# in the index first and then discover the tests, all in the same traversal.
38-
if @index.entries_for(uri.to_s)
39-
Listeners::TestStyle.new(@response_builder, @global_state, @dispatcher, @document.uri)
40-
Listeners::SpecStyle.new(@response_builder, @global_state, @dispatcher, @document.uri)
41-
42-
Addon.addons.each do |addon|
43-
addon.create_discover_tests_listener(@response_builder, @dispatcher, @document.uri)
44-
end
45-
46-
@dispatcher.visit(@document.ast)
47-
else
48-
@global_state.synchronize do
49-
RubyIndexer::DeclarationListener.new(
50-
@index,
51-
@dispatcher,
52-
@document.parse_result,
53-
uri,
54-
collect_comments: true,
55-
)
56-
57-
Listeners::TestStyle.new(@response_builder, @global_state, @dispatcher, @document.uri)
58-
Listeners::SpecStyle.new(@response_builder, @global_state, @dispatcher, @document.uri)
59-
60-
Addon.addons.each do |addon|
61-
addon.create_discover_tests_listener(@response_builder, @dispatcher, @document.uri)
62-
end
63-
64-
# Dispatch the events both for indexing the test file and discovering the tests. The order here is
65-
# important because we need the index to be aware of the existing classes/modules/methods before the test
66-
# listeners can do their work
67-
@dispatcher.visit(@document.ast)
68-
end
30+
Addon.addons.each do |addon|
31+
addon.create_discover_tests_listener(@response_builder, @dispatcher, @document.uri)
6932
end
7033

34+
@dispatcher.visit(@document.ast)
7135
@response_builder.response
7236
end
7337
end

test/requests/discover_tests_test.rb

Lines changed: 41 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -359,43 +359,6 @@ def test_something; end
359359
end
360360
end
361361

362-
def test_files_are_indexed_lazily_if_needed
363-
path = File.join(Dir.pwd, "lib", "foo.rb")
364-
File.write(path, <<~RUBY)
365-
class FooTest < Test::Unit::TestCase
366-
def test_something; end
367-
end
368-
RUBY
369-
370-
begin
371-
with_server do |server, uri|
372-
server.global_state.index.index_single(uri, <<~RUBY)
373-
module Test
374-
module Unit
375-
class TestCase; end
376-
end
377-
end
378-
RUBY
379-
380-
server.process_message(
381-
id: 1,
382-
method: "rubyLsp/discoverTests",
383-
params: { textDocument: { uri: URI::Generic.from_path(path: path) } },
384-
)
385-
386-
items = get_response(server)
387-
assert_equal(
388-
["FooTest"],
389-
items.map { |i| i[:label] },
390-
)
391-
assert_equal(["test_something"], items[0][:children].map { |i| i[:label] })
392-
assert_all_items_tagged_with(items, :test_unit)
393-
end
394-
ensure
395-
FileUtils.rm(path)
396-
end
397-
end
398-
399362
def test_does_not_raise_on_duplicate_test_ids
400363
source = <<~RUBY
401364
module Foo
@@ -898,13 +861,15 @@ def assert_all_items_tagged_with(items, tag)
898861
end
899862

900863
def with_minitest_test(source, &block)
901-
with_server(source) do |server, uri|
902-
server.global_state.index.index_single(uri, <<~RUBY)
903-
module Minitest
904-
class Test; end
905-
end
906-
RUBY
864+
source_with_minitest = <<~RUBY
865+
#{source}
907866
867+
module Minitest
868+
class Test; end
869+
end
870+
RUBY
871+
872+
with_server(source_with_minitest) do |server, uri|
908873
server.process_message(id: 1, method: "rubyLsp/discoverTests", params: {
909874
textDocument: { uri: uri },
910875
})
@@ -916,15 +881,17 @@ class Test; end
916881
end
917882

918883
def with_test_unit(source, &block)
919-
with_server(source) do |server, uri|
920-
server.global_state.index.index_single(uri, <<~RUBY)
921-
module Test
922-
module Unit
923-
class TestCase; end
924-
end
884+
source_with_test_unit = <<~RUBY
885+
#{source}
886+
887+
module Test
888+
module Unit
889+
class TestCase; end
925890
end
926-
RUBY
891+
end
892+
RUBY
927893

894+
with_server(source_with_test_unit) do |server, uri|
928895
server.process_message(id: 1, method: "rubyLsp/discoverTests", params: {
929896
textDocument: { uri: uri },
930897
})
@@ -936,24 +903,26 @@ class TestCase; end
936903
end
937904

938905
def with_active_support_declarative_tests(source, &block)
939-
with_server(source) do |server, uri|
940-
server.global_state.index.index_single(uri, <<~RUBY)
941-
module Minitest
942-
class Test; end
943-
end
906+
source_with_test_case = <<~RUBY
907+
#{source}
944908
945-
module ActiveSupport
946-
module Testing
947-
module Declarative
948-
end
949-
end
909+
module Minitest
910+
class Test; end
911+
end
950912
951-
class TestCase < Minitest::Test
952-
extend Testing::Declarative
913+
module ActiveSupport
914+
module Testing
915+
module Declarative
953916
end
954917
end
955-
RUBY
956918
919+
class TestCase < Minitest::Test
920+
extend Testing::Declarative
921+
end
922+
end
923+
RUBY
924+
925+
with_server(source_with_test_case) do |server, uri|
957926
server.process_message(id: 1, method: "rubyLsp/discoverTests", params: {
958927
textDocument: { uri: uri },
959928
})
@@ -965,14 +934,16 @@ class TestCase < Minitest::Test
965934
end
966935

967936
def with_minitest_spec_configured(source, &block)
968-
with_server(source) do |server, uri|
969-
server.global_state.index.index_single(uri, <<~RUBY)
970-
module Minitest
971-
class Test; end
972-
class Spec < Test; end
973-
end
974-
RUBY
937+
source_with_spec = <<~RUBY
938+
#{source}
939+
940+
module Minitest
941+
class Test; end
942+
class Spec < Test; end
943+
end
944+
RUBY
975945

946+
with_server(source_with_spec) do |server, uri|
976947
server.process_message(id: 1, method: "rubyLsp/discoverTests", params: {
977948
textDocument: { uri: uri },
978949
})

0 commit comments

Comments
 (0)