Skip to content

Commit 15c335a

Browse files
authored
Apply correct hierarchy and IDs to Minitest spec items (#3501)
* Add `spec` to load path when running Minitest specs * Apply correct hierarchy and IDs to Minitest spec items
1 parent f57377d commit 15c335a

File tree

4 files changed

+229
-89
lines changed

4 files changed

+229
-89
lines changed

lib/ruby_lsp/listeners/spec_style.rb

Lines changed: 73 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,24 @@
44
module 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

test/fixtures/minitest_spec_example.rb

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,31 @@
33
require "minitest/spec"
44
require "minitest/autorun"
55

6-
Minitest::Test.i_suck_and_my_tests_are_order_dependent!
6+
Minitest::Spec.i_suck_and_my_tests_are_order_dependent!
77

8-
class MySpec < Minitest::Spec
9-
describe "some scenario" do
10-
it "works as expected!" do
11-
assert_equal(1, 1)
12-
end
8+
module First
9+
module Second
10+
module Third
11+
class MySpec < Minitest::Spec
12+
# Anonymous example
13+
it do
14+
assert_equal(1, 1)
15+
end
16+
17+
describe "when something is true" do
18+
describe "and other thing is false" do
19+
it "does what's expected" do
20+
assert(true)
21+
end
22+
end
1323

14-
# Anonymous example
15-
it do
16-
assert_equal(2, 2)
24+
class NestedSpec < Minitest::Spec
25+
it "does something else" do
26+
assert(true)
27+
end
28+
end
29+
end
30+
end
1731
end
1832
end
1933
end

0 commit comments

Comments
 (0)