Class: RubyLsp::Listeners::TestStyle

Inherits:
TestDiscovery show all
Includes:
Requests::Support::Common
Defined in:
lib/ruby_lsp/listeners/test_style.rb

Constant Summary collapse

MINITEST_REPORTER_PATH =

: String

File.expand_path("../test_reporters/minitest_reporter.rb", __dir__)
TEST_UNIT_REPORTER_PATH =

: String

File.expand_path("../test_reporters/test_unit_reporter.rb", __dir__)
BASE_COMMAND =
begin
  Bundler.with_unbundled_env { Bundler.default_lockfile }
  "bundle exec ruby"
rescue Bundler::GemfileNotFound
  "ruby"
end
COMMAND =

: String

"#{BASE_COMMAND} -r#{MINITEST_REPORTER_PATH} -r#{TEST_UNIT_REPORTER_PATH}"
ACCESS_MODIFIERS =
[:public, :private, :protected].freeze

Constants inherited from TestDiscovery

RubyLsp::Listeners::TestDiscovery::DYNAMIC_REFERENCE_MARKER

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Requests::Support::Common

#categorized_markdown_from_index_entries, #constant_name, #create_code_lens, #each_constant_path_part, #kind_for_entry, #markdown_from_index_entries, #namespace_constant_name, #not_in_dependencies?, #range_from_location, #range_from_node, #self_receiver?

Constructor Details

#initialize(response_builder, global_state, dispatcher, uri) ⇒ TestStyle

: (ResponseBuilders::TestCollection, GlobalState, Prism::Dispatcher, URI::Generic) -> void



159
160
161
162
163
164
165
166
167
168
169
170
171
172
# File 'lib/ruby_lsp/listeners/test_style.rb', line 159

def initialize(response_builder, global_state, dispatcher, uri)
  super(response_builder, global_state, uri)

  @framework = :minitest #: Symbol
  @parent_stack = [@response_builder] #: Array[(Requests::Support::TestItem | ResponseBuilders::TestCollection)?]

  register_events(
    dispatcher,
    :on_class_node_enter,
    :on_def_node_enter,
    :on_call_node_enter,
    :on_call_node_leave,
  )
end

Class Method Details

.resolve_test_commands(items) ⇒ Object

Resolves the minimal set of commands required to execute the requested tests : (Array[Hash[Symbol, untyped]]) -> Array



10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
# File 'lib/ruby_lsp/listeners/test_style.rb', line 10

def resolve_test_commands(items)
  # A nested hash of file_path => test_group => { tags: [], examples: [test_example] } to ensure we build the
  # minimum amount of commands needed to execute the requested tests. This is only used for specific examples
  # where we will need more complex regexes to execute it all at the same time
  aggregated_tests = Hash.new do |hash, key|
    hash[key] = Hash.new do |inner_h, inner_k|
      inner_h[inner_k] = { tags: Set.new, examples: [] }
    end
  end

  # Full files are paths that should be executed as a whole e.g.: an entire test file or directory
  full_files = []
  queue = items.dup

  until queue.empty?
    item = queue.shift #: as !nil
    tags = Set.new(item[:tags])
    next unless tags.include?("framework:minitest") || tags.include?("framework:test_unit")

    children = item[:children]
    uri = URI(item[:uri])
    path = uri.full_path
    next unless path

    if tags.include?("test_dir")
      if children.empty?
        full_files.concat(
          Dir.glob(
            "#{path}/**/{*_test,test_*,*_spec}.rb",
            File::Constants::FNM_EXTGLOB | File::Constants::FNM_PATHNAME,
          ).map! { |p| Shellwords.escape(p) },
        )
      end
    elsif tags.include?("test_file")
      full_files << Shellwords.escape(path) if children.empty?
    elsif tags.include?("test_group")
      # If all of the children of the current test group are other groups, then there's no need to add it to the
      # aggregated examples
      unless children.any? && children.all? { |child| child[:tags].include?("test_group") }
        aggregated_tests[path][item[:id]] = { tags: tags, examples: [] }
      end
    else
      class_name, method_name = item[:id].split("#")
      aggregated_tests[path][class_name][:examples] << method_name
      aggregated_tests[path][class_name][:tags].merge(tags)
    end

    queue.concat(children) unless children.empty?
  end

  commands = []

  aggregated_tests.each do |file_path, groups_and_examples|
    # Separate groups into Minitest and Test Unit. You can have both frameworks in the same file, but you cannot
    # have a group belongs to both at the same time
    minitest_groups, test_unit_groups = groups_and_examples.partition do |_, info|
      info[:tags].include?("framework:minitest")
    end

    if minitest_groups.any?
      commands << handle_minitest_groups(file_path, minitest_groups)
    end

    if test_unit_groups.any?
      commands.concat(handle_test_unit_groups(file_path, test_unit_groups))
    end
  end

  unless full_files.empty?
    specs, tests = full_files.partition { |path| spec?(path) }

    commands << "#{COMMAND} -Itest -e \"ARGV.each { |f| require f }\" #{tests.join(" ")}" if tests.any?
    commands << "#{COMMAND} -Ispec -e \"ARGV.each { |f| require f }\" #{specs.join(" ")}" if specs.any?
  end

  commands
end

Instance Method Details

#on_call_node_enter(node) ⇒ Object

: (Prism::CallNode node) -> void



238
239
240
241
242
243
# File 'lib/ruby_lsp/listeners/test_style.rb', line 238

def on_call_node_enter(node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod
  name = node.name
  return unless ACCESS_MODIFIERS.include?(name)

  @visibility_stack << name
end

#on_call_node_leave(node) ⇒ Object

: (Prism::CallNode node) -> void



246
247
248
249
250
251
252
# File 'lib/ruby_lsp/listeners/test_style.rb', line 246

def on_call_node_leave(node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod
  name = node.name
  return unless ACCESS_MODIFIERS.include?(name)
  return unless node.arguments&.arguments

  @visibility_stack.pop
end

#on_class_node_enter(node) ⇒ Object

: (Prism::ClassNode node) -> void



175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
# File 'lib/ruby_lsp/listeners/test_style.rb', line 175

def on_class_node_enter(node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod
  with_test_ancestor_tracking(node) do |name, ancestors|
    @framework = :test_unit if ancestors.include?("Test::Unit::TestCase")

    if @framework == :test_unit || non_declarative_minitest?(ancestors, name)
      test_item = Requests::Support::TestItem.new(
        name,
        name,
        @uri,
        range_from_node(node),
        framework: @framework,
      )

      last_test_group.add(test_item)
      @response_builder.add_code_lens(test_item)
      @parent_stack << test_item
    else
      @parent_stack << nil
    end
  end
end

#on_class_node_leave(node) ⇒ Object

: (Prism::ClassNode node) -> void



198
199
200
201
# File 'lib/ruby_lsp/listeners/test_style.rb', line 198

def on_class_node_leave(node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod
  @parent_stack.pop
  super
end

#on_def_node_enter(node) ⇒ Object

: (Prism::DefNode node) -> void



216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
# File 'lib/ruby_lsp/listeners/test_style.rb', line 216

def on_def_node_enter(node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod
  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("::")
  parent = @parent_stack.last
  return unless parent.is_a?(Requests::Support::TestItem)

  example_item = Requests::Support::TestItem.new(
    "#{current_group_name}##{name}",
    name,
    @uri,
    range_from_node(node),
    framework: @framework,
  )
  parent.add(example_item)
  @response_builder.add_code_lens(example_item)
end

#on_module_node_enter(node) ⇒ Object

: (Prism::ModuleNode node) -> void



204
205
206
207
# File 'lib/ruby_lsp/listeners/test_style.rb', line 204

def on_module_node_enter(node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod
  @parent_stack << nil
  super
end

#on_module_node_leave(node) ⇒ Object

: (Prism::ModuleNode node) -> void



210
211
212
213
# File 'lib/ruby_lsp/listeners/test_style.rb', line 210

def on_module_node_leave(node) # rubocop:disable RubyLsp/UseRegisterWithHandlerMethod
  @parent_stack.pop
  super
end