-
Notifications
You must be signed in to change notification settings - Fork 182
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add discover tests custom request (#3180)
Add discover tests custom request (#3180) ### Motivation Closes #3171 Add a new custom request to discover tests in a specific document. This request will instantiate all listeners and collect all of the discovered groups and examples as test items for the editor. ### Implementation - Created the new request - Implement a listener dedicated to the test style syntax only (spec will be a separate listener) - Ensured to use ancestor linearization to determine the test framework ### Automated Tests Added tests. Co-authored-by: vinistock <[email protected]>
- Loading branch information
Showing
7 changed files
with
552 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,166 @@ | ||
# typed: strict | ||
# frozen_string_literal: true | ||
|
||
module RubyLsp | ||
module Listeners | ||
class TestStyle | ||
extend T::Sig | ||
include Requests::Support::Common | ||
|
||
ACCESS_MODIFIERS = [:public, :private, :protected].freeze | ||
DYNAMIC_REFERENCE_MARKER = "<dynamic_reference>" | ||
|
||
sig do | ||
params( | ||
response_builder: ResponseBuilders::TestCollection, | ||
global_state: GlobalState, | ||
dispatcher: Prism::Dispatcher, | ||
uri: URI::Generic, | ||
).void | ||
end | ||
def initialize(response_builder, global_state, dispatcher, uri) | ||
@response_builder = response_builder | ||
@global_state = global_state | ||
@uri = uri | ||
@index = T.let(global_state.index, RubyIndexer::Index) | ||
|
||
@visibility_stack = T.let([:public], T::Array[Symbol]) | ||
@nesting = T.let([], T::Array[String]) | ||
|
||
dispatcher.register( | ||
self, | ||
:on_class_node_enter, | ||
:on_class_node_leave, | ||
:on_module_node_enter, | ||
:on_module_node_leave, | ||
:on_def_node_enter, | ||
:on_call_node_enter, | ||
:on_call_node_leave, | ||
) | ||
end | ||
|
||
sig { params(node: Prism::ClassNode).void } | ||
def on_class_node_enter(node) | ||
@visibility_stack << :public | ||
name = constant_name(node.constant_path) | ||
name ||= name_with_dynamic_reference(node.constant_path) | ||
|
||
fully_qualified_name = RubyIndexer::Index.actual_nesting(@nesting, name).join("::") | ||
|
||
attached_ancestors = begin | ||
@index.linearized_ancestors_of(fully_qualified_name) | ||
rescue RubyIndexer::Index::NonExistingNamespaceError | ||
# When there are dynamic parts in the constant path, we will not have indexed the namespace. We can still | ||
# provide test functionality if the class inherits directly from Test::Unit::TestCase or Minitest::Test | ||
[node.superclass&.slice].compact | ||
end | ||
|
||
if attached_ancestors.include?("Test::Unit::TestCase") || | ||
non_declarative_minitest?(attached_ancestors, fully_qualified_name) | ||
|
||
@response_builder.add(Requests::Support::TestItem.new( | ||
fully_qualified_name, | ||
fully_qualified_name, | ||
@uri, | ||
range_from_node(node), | ||
)) | ||
end | ||
|
||
@nesting << name | ||
end | ||
|
||
sig { params(node: Prism::ModuleNode).void } | ||
def on_module_node_enter(node) | ||
@visibility_stack << :public | ||
|
||
name = constant_name(node.constant_path) | ||
name ||= name_with_dynamic_reference(node.constant_path) | ||
|
||
@nesting << name | ||
end | ||
|
||
sig { params(node: Prism::ModuleNode).void } | ||
def on_module_node_leave(node) | ||
@visibility_stack.pop | ||
@nesting.pop | ||
end | ||
|
||
sig { params(node: Prism::ClassNode).void } | ||
def on_class_node_leave(node) | ||
@visibility_stack.pop | ||
@nesting.pop | ||
end | ||
|
||
sig { params(node: Prism::DefNode).void } | ||
def on_def_node_enter(node) | ||
return if @visibility_stack.last != :public | ||
|
||
name = node.name.to_s | ||
return unless name.start_with?("test_") | ||
|
||
current_group_name = RubyIndexer::Index.actual_nesting(@nesting, nil).join("::") | ||
|
||
# If we're finding a test method, but for the wrong framework, then the group test item will not have been | ||
# previously pushed and thus we return early and avoid adding items for a framework this listener is not | ||
# interested in | ||
test_item = @response_builder[current_group_name] | ||
return unless test_item | ||
|
||
test_item.add(Requests::Support::TestItem.new( | ||
"#{current_group_name}##{name}", | ||
name, | ||
@uri, | ||
range_from_node(node), | ||
)) | ||
end | ||
|
||
sig { params(node: Prism::CallNode).void } | ||
def on_call_node_enter(node) | ||
name = node.name | ||
return unless ACCESS_MODIFIERS.include?(name) | ||
|
||
@visibility_stack << name | ||
end | ||
|
||
sig { params(node: Prism::CallNode).void } | ||
def on_call_node_leave(node) | ||
name = node.name | ||
return unless ACCESS_MODIFIERS.include?(name) | ||
return unless node.arguments&.arguments | ||
|
||
@visibility_stack.pop | ||
end | ||
|
||
private | ||
|
||
sig { params(attached_ancestors: T::Array[String], fully_qualified_name: String).returns(T::Boolean) } | ||
def non_declarative_minitest?(attached_ancestors, fully_qualified_name) | ||
return false unless attached_ancestors.include?("Minitest::Test") | ||
|
||
# We only support regular Minitest tests. The declarative syntax provided by ActiveSupport is handled by the | ||
# Rails add-on | ||
name_parts = fully_qualified_name.split("::") | ||
singleton_name = "#{name_parts.join("::")}::<Class:#{name_parts.last}>" | ||
!@index.linearized_ancestors_of(singleton_name).include?("ActiveSupport::Testing::Declarative") | ||
rescue RubyIndexer::Index::NonExistingNamespaceError | ||
true | ||
end | ||
|
||
sig do | ||
params( | ||
node: T.any( | ||
Prism::ConstantPathNode, | ||
Prism::ConstantReadNode, | ||
Prism::ConstantPathTargetNode, | ||
Prism::CallNode, | ||
Prism::MissingNode, | ||
), | ||
).returns(String) | ||
end | ||
def name_with_dynamic_reference(node) | ||
slice = node.slice | ||
slice.gsub(/((?<=::)|^)[a-z]\w*/, DYNAMIC_REFERENCE_MARKER) | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
# typed: strict | ||
# frozen_string_literal: true | ||
|
||
require "ruby_lsp/listeners/test_style" | ||
|
||
module RubyLsp | ||
module Requests | ||
# This is a custom request to ask the server to parse a test file and discover all available examples in it. Add-ons | ||
# can augment the behavior through listeners, allowing them to handle discovery for different frameworks | ||
class DiscoverTests < Request | ||
extend T::Sig | ||
include Support::Common | ||
|
||
sig { params(global_state: GlobalState, document: RubyDocument, dispatcher: Prism::Dispatcher).void } | ||
def initialize(global_state, document, dispatcher) | ||
super() | ||
@global_state = global_state | ||
@document = document | ||
@dispatcher = dispatcher | ||
@response_builder = T.let(ResponseBuilders::TestCollection.new, ResponseBuilders::TestCollection) | ||
@index = T.let(global_state.index, RubyIndexer::Index) | ||
end | ||
|
||
sig { override.returns(T::Array[Support::TestItem]) } | ||
def perform | ||
uri = @document.uri | ||
|
||
# We normally only index test files once they are opened in the editor to save memory and avoid doing | ||
# unnecessary work. If the file is already opened and we already indexed it, then we can just discover the tests | ||
# straight away. | ||
# | ||
# However, if the user navigates to a specific test file from the explorer with nothing opened in the UI, then | ||
# we will not have indexed the test file yet and trying to linearize the ancestor of the class will fail. In | ||
# this case, we have to instantiate the indexer listener first, so that we insert classes, modules and methods | ||
# in the index first and then discover the tests, all in the same traversal. | ||
if @index.entries_for(uri.to_s) | ||
Listeners::TestStyle.new(@response_builder, @global_state, @dispatcher, @document.uri) | ||
@dispatcher.visit(@document.parse_result.value) | ||
else | ||
@global_state.synchronize do | ||
RubyIndexer::DeclarationListener.new( | ||
@index, | ||
@dispatcher, | ||
@document.parse_result, | ||
uri, | ||
collect_comments: true, | ||
) | ||
|
||
Listeners::TestStyle.new(@response_builder, @global_state, @dispatcher, @document.uri) | ||
|
||
# Dispatch the events both for indexing the test file and discovering the tests. The order here is | ||
# important because we need the index to be aware of the existing classes/modules/methods before the test | ||
# listeners can do their work | ||
@dispatcher.visit(@document.parse_result.value) | ||
end | ||
end | ||
|
||
@response_builder.response | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -103,6 +103,7 @@ unaliased | |
unindexed | ||
unparser | ||
unresolve | ||
vcall | ||
Vinicius | ||
vscodemachineid | ||
vsctm | ||
|
Oops, something went wrong.