Class: RubyLsp::LspReporter

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

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeLspReporter

: -> void



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

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
end

Class Method Details

.executed_under_test_runner?Boolean

: -> bool

Returns:

  • (Boolean)


205
206
207
# File 'lib/ruby_lsp/test_reporters/lsp_reporter.rb', line 205

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

.start_coverage?Boolean

: -> bool

Returns:

  • (Boolean)


200
201
202
# File 'lib/ruby_lsp/test_reporters/lsp_reporter.rb', line 200

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

Instance Method Details

#at_coverage_exitObject

: -> void



187
188
189
190
191
# File 'lib/ruby_lsp/test_reporters/lsp_reporter.rb', line 187

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



194
195
196
# File 'lib/ruby_lsp/test_reporters/lsp_reporter.rb', line 194

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]



138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
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
# File 'lib/ruby_lsp/test_reporters/lsp_reporter.rb', line 138

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



67
68
69
70
71
72
# File 'lib/ruby_lsp/test_reporters/lsp_reporter.rb', line 67

def internal_shutdown
  @invoked_shutdown = true

  send_message("finish")
  @io.close
end

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

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



95
96
97
# File 'lib/ruby_lsp/test_reporters/lsp_reporter.rb', line 95

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



85
86
87
# File 'lib/ruby_lsp/test_reporters/lsp_reporter.rb', line 85

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



80
81
82
# File 'lib/ruby_lsp/test_reporters/lsp_reporter.rb', line 80

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



90
91
92
# File 'lib/ruby_lsp/test_reporters/lsp_reporter.rb', line 90

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

#shutdownObject

: -> void



56
57
58
59
60
61
62
# File 'lib/ruby_lsp/test_reporters/lsp_reporter.rb', line 56

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



75
76
77
# File 'lib/ruby_lsp/test_reporters/lsp_reporter.rb', line 75

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

#uri_and_line_for(method_object) ⇒ Object

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



100
101
102
103
104
105
106
107
108
# File 'lib/ruby_lsp/test_reporters/lsp_reporter.rb', line 100

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