Class: RubyLsp::LspReporter

Inherits:
Object
  • Object
show all
Defined in:
lib/ruby_lsp/test_reporters/lsp_reporter.rb

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeLspReporter

: -> void



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
# File 'lib/ruby_lsp/test_reporters/lsp_reporter.rb', line 55

def initialize
  dir_path = File.join(Dir.tmpdir, "ruby-lsp")
  FileUtils.mkdir_p(dir_path)

  port_db_path = File.join(dir_path, "test_reporter_port_db.json")
  port = ENV["RUBY_LSP_REPORTER_PORT"]

  @io = begin
    # The environment variable is only used for tests. The extension always writes to the temporary file
    if port
      socket(port)
    elsif File.exist?(port_db_path)
      db = JSON.load_file(port_db_path)
      socket(db[Dir.pwd])
    else
      # For tests that don't spawn the TCP server
      require "stringio"
      StringIO.new
    end
  rescue
    require "stringio"
    StringIO.new
  end #: IO | StringIO

  @invoked_shutdown = false #: bool
  @message_queue = Thread::Queue.new #: Thread::Queue
  @writer = Thread.new { write_loop } #: Thread
end

Class Method Details

.executed_under_test_runner?Boolean

: -> bool

Returns:

  • (Boolean)


26
27
28
# File 'lib/ruby_lsp/test_reporters/lsp_reporter.rb', line 26

def executed_under_test_runner?
  !!(ENV["RUBY_LSP_TEST_RUNNER"] && ENV["RUBY_LSP_ENV"] != "test")
end

.instanceObject

: -> LspReporter



16
17
18
# File 'lib/ruby_lsp/test_reporters/lsp_reporter.rb', line 16

def instance
  @instance ||= new
end

.start_coverage?Boolean

: -> bool

Returns:

  • (Boolean)


21
22
23
# File 'lib/ruby_lsp/test_reporters/lsp_reporter.rb', line 21

def start_coverage?
  ENV["RUBY_LSP_TEST_RUNNER"] == "coverage"
end

.uri_and_line_for(method_object) ⇒ Object

: (Method | UnboundMethod) -> [URI::Generic, Integer?]?



31
32
33
34
35
36
37
38
39
# File 'lib/ruby_lsp/test_reporters/lsp_reporter.rb', line 31

def uri_and_line_for(method_object)
  file_path, line = method_object.source_location
  return unless file_path
  return if file_path.start_with?("(eval at ")

  uri = URI::Generic.from_path(path: File.expand_path(file_path))
  zero_based_line = line ? line - 1 : nil
  [uri, zero_based_line]
end

Instance Method Details

#at_coverage_exitObject

: -> void



207
208
209
210
211
# File 'lib/ruby_lsp/test_reporters/lsp_reporter.rb', line 207

def at_coverage_exit
  coverage_results = gather_coverage_results
  File.write(File.join(".ruby-lsp", "coverage_result.json"), coverage_results.to_json)
  internal_shutdown
end

#at_exitObject

: -> void



214
215
216
# File 'lib/ruby_lsp/test_reporters/lsp_reporter.rb', line 214

def at_exit
  internal_shutdown unless @invoked_shutdown
end

#gather_coverage_resultsObject

Gather the results returned by Coverage.result and format like the VS Code test explorer expects

Coverage result format:

Lines are reported in order as an array where each number is the number of times it was executed. For example, the following says that line 0 was executed 1 time and line 1 executed 3 times: [1, 3]. Nil values represent lines for which coverage is not available, like empty lines, comments or keywords like else

Branches are a hash containing the name of the branch and the location where it is found in tuples with the following elements: [NAME, ID, START_LINE, START_COLUMN, END_LINE, END_COLUMN] as the keys and the value is the number of times it was executed

Methods are a similar hash [ClassName, :method_name, START_LINE, START_COLUMN, END_LINE, END_COLUMN] => NUMBER OF EXECUTIONS

Example: {

"file_path" => {
  "lines" => [1, 2, 3, nil],
  "branches" => {
    ["&.", 0, 6, 21, 6, 65] => { [:then, 1, 6, 21, 6, 65] => 0, [:else, 5, 7, 0, 7, 87] => 1 }
  },
  "methods" => {
    ["Foo", :bar, 6, 21, 6, 65] => 0
  }

} : -> Hash[String, statement_coverage]



158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
# File 'lib/ruby_lsp/test_reporters/lsp_reporter.rb', line 158

def gather_coverage_results
  # Ignore coverage results inside dependencies
  bundle_path = Bundler.bundle_path.to_s

  result = Coverage.result.reject do |file_path, _coverage_info|
    file_path.start_with?(bundle_path) || !file_path.start_with?(Dir.pwd)
  end

  result.to_h do |file_path, coverage_info|
    # Format the branch coverage information as VS Code expects it and then group it based on the start line of
    # the conditional that causes the branching. We need to match each line coverage data with the branches that
    # spawn from that line
     = coverage_info[:branches]
      .flat_map do |branch, data|
        branch_name, _branch_id, branch_start_line, _branch_start_col, _branch_end_line, _branch_end_col = branch

        data.map do |then_or_else, execution_count|
          name, _id, start_line, start_column, end_line, end_column = then_or_else

          {
            groupingLine: branch_start_line,
            executed: execution_count,
            location: {
              start: { line: start_line, character: start_column },
              end: { line: end_line, character: end_column },
            },
            label: "#{branch_name} #{name}",
          }
        end
      end
      .group_by { |branch| branch[:groupingLine] }

    # Format the line coverage information, gathering any branch coverage data associated with that line
    data = coverage_info[:lines].filter_map.with_index do |execution_count, line_index|
      next if execution_count.nil?

      {
        executed: execution_count,
        location: { line: line_index, character: 0 },
        branches: [line_index] || [],
      }
    end

    # The expected format is URI => { executed: number_of_times_executed, location: { ... }, branches: [ ... ] }
    [URI::Generic.from_path(path: File.expand_path(file_path)).to_s, data]
  end
end

#internal_shutdownObject

This method is intended to be used by the RubyLsp::LspReporter class itself only. If you’re writing a custom test reporter, use shutdown instead : -> void



96
97
98
99
100
101
102
103
# File 'lib/ruby_lsp/test_reporters/lsp_reporter.rb', line 96

def internal_shutdown
  @invoked_shutdown = true

  send_message("finish")
  @message_queue.close
  @writer.join
  @io.close
end

#record_error(id:, message:, uri:) ⇒ Object

: (id: String, message: String?, uri: URI::Generic) -> void



126
127
128
# File 'lib/ruby_lsp/test_reporters/lsp_reporter.rb', line 126

def record_error(id:, message:, uri:)
  send_message("error", id: id, message: message, uri: uri.to_s)
end

#record_fail(id:, message:, uri:) ⇒ Object

: (id: String, message: String, uri: URI::Generic) -> void



116
117
118
# File 'lib/ruby_lsp/test_reporters/lsp_reporter.rb', line 116

def record_fail(id:, message:, uri:)
  send_message("fail", id: id, message: message, uri: uri.to_s)
end

#record_pass(id:, uri:) ⇒ Object

: (id: String, uri: URI::Generic) -> void



111
112
113
# File 'lib/ruby_lsp/test_reporters/lsp_reporter.rb', line 111

def record_pass(id:, uri:)
  send_message("pass", id: id, uri: uri.to_s)
end

#record_skip(id:, uri:) ⇒ Object

: (id: String, uri: URI::Generic) -> void



121
122
123
# File 'lib/ruby_lsp/test_reporters/lsp_reporter.rb', line 121

def record_skip(id:, uri:)
  send_message("skip", id: id, uri: uri.to_s)
end

#shutdownObject

: -> void



85
86
87
88
89
90
91
# File 'lib/ruby_lsp/test_reporters/lsp_reporter.rb', line 85

def shutdown
  # When running in coverage mode, we don't want to inform the extension that we finished immediately after running
  # tests. We only do it after we finish processing coverage results, by invoking `internal_shutdown`
  return if ENV["RUBY_LSP_TEST_RUNNER"] == "coverage"

  internal_shutdown
end

#start_test(id:, uri:, line: nil) ⇒ Object

: (id: String, uri: URI::Generic, ?line: Integer?) -> void



106
107
108
# File 'lib/ruby_lsp/test_reporters/lsp_reporter.rb', line 106

def start_test(id:, uri:, line: nil)
  send_message("start", id: id, uri: uri.to_s, line: line)
end