44module RubyLsp
55 module Listeners
66 class SpecStyle < TestDiscovery
7+ class Group
8+ #: String
9+ attr_reader :id
10+
11+ #: (String) -> void
12+ def initialize ( id )
13+ @id = id
14+ end
15+ end
16+
17+ class ClassGroup < Group ; end
18+ class DescribeGroup < Group ; end
19+
720 #: (ResponseBuilders::TestCollection, GlobalState, Prism::Dispatcher, URI::Generic) -> void
821 def initialize ( response_builder , global_state , dispatcher , uri )
922 super
1023
11- @describe_block_nesting = [ ] #: Array[String]
12- @spec_class_stack = [ ] #: Array[bool]
24+ @spec_group_id_stack = [ ] #: Array[Group?]
1325
1426 dispatcher . register (
1527 self ,
@@ -22,21 +34,21 @@ def initialize(response_builder, global_state, dispatcher, uri)
2234
2335 #: (Prism::ClassNode) -> void
2436 def on_class_node_enter ( node )
25- with_test_ancestor_tracking ( node ) do |_ , ancestors |
26- is_spec = ancestors . include? ( "Minitest::Spec" )
27- @spec_class_stack . push ( is_spec )
37+ with_test_ancestor_tracking ( node ) do |name , ancestors |
38+ @spec_group_id_stack << ( ancestors . include? ( "Minitest::Spec" ) ? ClassGroup . new ( name ) : nil )
2839 end
2940 end
3041
3142 #: (Prism::ClassNode) -> void
3243 def on_class_node_leave ( node ) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod
3344 super
34-
35- @spec_class_stack . pop
45+ @spec_group_id_stack . pop
3646 end
3747
3848 #: (Prism::CallNode) -> void
3949 def on_call_node_enter ( node )
50+ return unless in_spec_context?
51+
4052 case node . name
4153 when :describe
4254 handle_describe ( node )
@@ -49,85 +61,63 @@ def on_call_node_enter(node)
4961 def on_call_node_leave ( node )
5062 return unless node . name == :describe && !node . receiver
5163
52- @describe_block_nesting . pop
64+ @spec_group_id_stack . pop
5365 end
5466
5567 private
5668
5769 #: (Prism::CallNode) -> void
5870 def handle_describe ( node )
71+ # Describes will include the nesting of all classes and all outer describes as part of its ID, unlike classes
72+ # that ignore describes
5973 return if node . block . nil?
60- return unless in_spec_context?
6174
6275 description = extract_description ( node )
6376 return unless description
6477
65- if @nesting . empty? && @describe_block_nesting . empty?
66- test_item = Requests ::Support ::TestItem . new (
67- description ,
68- description ,
69- @uri ,
70- range_from_node ( node ) ,
71- framework : :minitest ,
72- )
73- @response_builder . add ( test_item )
74- @response_builder . add_code_lens ( test_item )
78+ parent = latest_group
79+ id = case parent
80+ when Requests ::Support ::TestItem
81+ "#{ parent . id } ::#{ description } "
7582 else
76- add_to_parent_test_group ( description , node )
83+ description
7784 end
7885
79- @describe_block_nesting << description
86+ test_item = Requests ::Support ::TestItem . new (
87+ id ,
88+ description ,
89+ @uri ,
90+ range_from_node ( node ) ,
91+ framework : :minitest ,
92+ )
93+
94+ parent . add ( test_item )
95+ @response_builder . add_code_lens ( test_item )
96+ @spec_group_id_stack << DescribeGroup . new ( id )
8097 end
8198
8299 #: (Prism::CallNode) -> void
83100 def handle_example ( node )
84- return unless in_spec_context?
85- return if @describe_block_nesting . empty? && @nesting . empty?
86-
87101 # Minitest formats the descriptions into test method names by using the count of examples with the description
88102 # We are not guaranteed to discover examples in the exact order using static analysis, so we use the line number
89103 # instead. Note that anonymous examples mixed with meta-programming will not be handled correctly
90104 description = extract_description ( node ) || "anonymous"
91- line = node . location . start_line
105+ line = node . location . start_line - 1
106+ parent = latest_group
107+ return unless parent . is_a? ( Requests ::Support ::TestItem )
92108
93- add_to_parent_test_group ( format ( "test_%04d_%s" , line , description ) , node )
94- end
95-
96- #: (String, Prism::CallNode) -> void
97- def add_to_parent_test_group ( description , node )
98- parent_test_group = find_parent_test_group
99- return unless parent_test_group
109+ id = "#{ parent . id } ##{ format ( "test_%04d_%s" , line , description ) } "
100110
101111 test_item = Requests ::Support ::TestItem . new (
102- description ,
112+ id ,
103113 description ,
104114 @uri ,
105115 range_from_node ( node ) ,
106116 framework : :minitest ,
107117 )
108- parent_test_group . add ( test_item )
109- @response_builder . add_code_lens ( test_item )
110- end
111-
112- #: -> Requests::Support::TestItem?
113- def find_parent_test_group
114- root_group_name , nested_describe_groups = if @nesting . empty?
115- [ @describe_block_nesting . first , @describe_block_nesting [ 1 ..] ]
116- else
117- [ RubyIndexer ::Index . actual_nesting ( @nesting , nil ) . join ( "::" ) , @describe_block_nesting ]
118- end
119- return unless root_group_name
120-
121- test_group = @response_builder [ root_group_name ] #: Requests::Support::TestItem?
122- return unless test_group
123118
124- return test_group unless nested_describe_groups
125-
126- nested_describe_groups . each do |description |
127- test_group = test_group [ description ]
128- end
129-
130- test_group
119+ parent . add ( test_item )
120+ @response_builder . add_code_lens ( test_item )
131121 end
132122
133123 #: (Prism::CallNode) -> String?
@@ -145,11 +135,36 @@ def extract_description(node)
145135 end
146136 end
147137
138+ #: -> (Requests::Support::TestItem | ResponseBuilders::TestCollection)
139+ def latest_group
140+ return @response_builder if @spec_group_id_stack . compact . empty?
141+
142+ first_class_index = @spec_group_id_stack . rindex { |i | i . is_a? ( ClassGroup ) } || 0
143+ first_class = @spec_group_id_stack [ 0 ] #: as !nil
144+ item = @response_builder [ first_class . id ] #: as !nil
145+
146+ # Descend into child items from the beginning all the way to the latest class group, ignoring describes
147+ @spec_group_id_stack [ 1 ..first_class_index ] #: as !nil
148+ . each do |group |
149+ next unless group . is_a? ( ClassGroup )
150+
151+ item = item [ group . id ] #: as !nil
152+ end
153+
154+ # From the class forward, we must take describes into account
155+ @spec_group_id_stack [ first_class_index + 1 ..] #: as !nil
156+ . each do |group |
157+ next unless group
158+
159+ item = item [ group . id ] #: as !nil
160+ end
161+
162+ item
163+ end
164+
148165 #: -> bool
149166 def in_spec_context?
150- return true if @nesting . empty?
151-
152- @spec_class_stack . last #: as !nil
167+ @nesting . empty? || @spec_group_id_stack . any? { |id | id }
153168 end
154169 end
155170 end
0 commit comments