Skip to content

Commit

Permalink
Add test-unit reporter
Browse files Browse the repository at this point in the history
  • Loading branch information
andyw8 committed Feb 26, 2025
1 parent d25ac1b commit 12dd221
Show file tree
Hide file tree
Showing 12 changed files with 5,747 additions and 60 deletions.
2 changes: 2 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ Sorbet/StrictSigil:
- "lib/ruby_lsp/load_sorbet.rb"
- "lib/ruby_lsp/scripts/compose_bundle.rb"
- "lib/ruby_lsp/test_helper.rb"
- "lib/ruby_lsp/test_reporter.rb" # runs as part of host app where sorbet-runtime may not be available
- "lib/ruby_lsp/test_unit_test_runner.rb" # runs as part of host app where sorbet-runtime may not be available

Layout/ClassStructure:
Enabled: true
Expand Down
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ group :development do
gem "rubocop", "~> 1.70"
gem "simplecov", require: false
gem "syntax_tree", ">= 6.1.1", "< 7"
gem "test-unit", require: false

platforms :ruby do # C Ruby (MRI), Rubinius or TruffleRuby, but NOT Windows
# sorbet-static is not available on Windows. We also skip Tapioca since it depends on sorbet-static-and-runtime
Expand Down
4 changes: 4 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ GEM
parser (3.3.7.1)
ast (~> 2.4.1)
racc
power_assert (2.0.5)
prettier_print (1.2.1)
prism (1.3.0)
psych (5.1.2)
Expand Down Expand Up @@ -116,6 +117,8 @@ GEM
spoom (>= 1.2.0)
thor (>= 1.2.0)
yard-sorbet
test-unit (3.6.7)
power_assert
thor (1.3.2)
unicode-display_width (3.1.4)
unicode-emoji (~> 4.0, >= 4.0.4)
Expand Down Expand Up @@ -151,6 +154,7 @@ DEPENDENCIES
sorbet-static-and-runtime
syntax_tree (>= 6.1.1, < 7)
tapioca (~> 0.16)
test-unit

BUNDLED WITH
2.6.2
71 changes: 71 additions & 0 deletions lib/ruby_lsp/test_reporter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# typed: false
# frozen_string_literal: true

require "json"

module RubyLsp
module TestReporter
class << self
#: (id: String, file: String) -> void
def start_test(id:, file:)
result = {
event: "start",
id: id,
file: file,
}
send_message(result)
end

#: (id: String, file: String) -> void
def record_pass(id:, file:)
result = {
event: "pass",
id: id,
file: file,
}
send_message(result)
end

#: (id: String, type: untyped, message: String, file: String) -> void
def record_fail(id:, type:, message:, file:)
result = {
event: "fail",
id: id,
type: type,
message: message,
file: file,
}
send_message(result)
end

#: (id: String, message: String?, file: String) -> void
def record_skip(id:, message:, file:)
result = {
event: "skip",
id: id,
message: message,
file: file,
}
send_message(result)
end

#: (id: String, message: String?, file: String) -> void
def record_error(id:, message:, file:)
result = {
event: "error",
id: id,
message: message,
file: file,
}
send_message(result)
end

private

def send_message(result)
json_message = result.to_json
$stdout.write("Content-Length: #{json_message.bytesize}\r\n\r\n#{json_message}")
end
end
end
end
100 changes: 100 additions & 0 deletions lib/ruby_lsp/test_unit_test_runner.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# typed: false
# frozen_string_literal: true

require "test/unit"
require "test/unit/ui/testrunner"
require "stringio"
require "ruby_lsp/test_reporter"

module RubyLsp
class TestRunner < ::Test::Unit::UI::TestRunner
#: (::Test::Unit::TestCase test) -> void
def test_started(test)
current_test = test
@current_file = file_for_test(current_test)
@current_test_id = "#{current_test.class.name}##{current_test.method_name}"
result = {
id: @current_test_id,
file: @current_file,
}
TestReporter.start_test(**result)
end

#: (::Test::Unit::TestCase test) -> void
def test_finished(test)
if test.passed?
# tests with an Omission are still marked as passed, which seems strange
# return if test.instance_variable_get("@_result").faults.any?

result = {
id: @current_test_id,
file: @current_file,
}
TestReporter.record_pass(**result)
end
end

#: (::Test::Unit::Failure | ::Test::Unit::Omission | ::Test::Unit::Error result) -> void
def result_fault(result)
case result
when ::Test::Unit::Failure
record_failure(result)
when ::Test::Unit::Error
record_error(result)
when ::Test::Unit::Pending
record_skip(result)
end
end

#: (::Test::Unit::Failure failure) -> void
def record_failure(failure)
result = {
id: @current_test_id,
type: failure.class.name,
message: failure.message,
file: @current_file,
}
TestReporter.record_fail(**result)
end

#: (::Test::Unit::Error error) -> void
def record_error(error)
result = {
id: @current_test_id,
message: error.message,
file: @current_file,
}
TestReporter.record_error(**result)
end

#: (::Test::Unit::Omission omission) -> void
def record_skip(omission)
result = {
id: @current_test_id,
message: omission.message,
file: @current_file,
}
TestReporter.record_skip(**result)
end

#: (::Test::Unit::TestCase test) -> String
def file_for_test(test)
location = test.method(test.method_name).source_location
return "" unless location # TODO: when might this be nil?

file, _line = location
return "" if file.start_with?("(eval at ") # test is dynamically defined (TODO: better way to check?)

file
end

#: -> void
def attach_to_mediator
@mediator.add_listener(Test::Unit::TestResult::FAULT, &method(:result_fault))
@mediator.add_listener(Test::Unit::TestCase::STARTED_OBJECT, &method(:test_started))
@mediator.add_listener(Test::Unit::TestCase::FINISHED_OBJECT, &method(:test_finished))
end
end
end

Test::Unit::AutoRunner.register_runner(:ruby_lsp) { |_auto_runner| RubyLsp::TestRunner }
3 changes: 3 additions & 0 deletions project-words
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,9 @@ subexpression
supertypes
Supertypes
suppo
testresult
testrunner
testrunnermediator
truffleruby
unaliased
unindexed
Expand Down
Loading

0 comments on commit 12dd221

Please sign in to comment.