Skip to content

Commit

Permalink
Add test-unit reporter (#3238)
Browse files Browse the repository at this point in the history
Addresses part of #3176

Note: The `omission` feature in test-unit is not currently supported.
  • Loading branch information
andyw8 committed Feb 28, 2025
1 parent 87a02dc commit 08ec2e7
Show file tree
Hide file tree
Showing 14 changed files with 5,777 additions and 65 deletions.
1 change: 1 addition & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ 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_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
10 changes: 5 additions & 5 deletions lib/ruby_indexer/lib/ruby_indexer/uri.rb
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
# typed: strict
# frozen_string_literal: true

require "uri"

module URI
class Generic
extend T::Sig

# Avoid a deprecation warning with Ruby 3.4 where the default parser was changed to RFC3986.
# This condition must remain even after support for 3.4 has been dropped for users that have
# `uri` in their lockfile, decoupling it from the ruby version.
PARSER = T.let(const_defined?(:RFC2396_PARSER) ? RFC2396_PARSER : DEFAULT_PARSER, RFC2396_Parser)

class << self
extend T::Sig
# NOTE: We also define this in the shim
PARSER = const_defined?(:RFC2396_PARSER) ? RFC2396_PARSER : DEFAULT_PARSER

class << self
#: (path: String, ?fragment: String?, ?scheme: String, ?load_path_entry: String?) -> URI::Generic
def from_path(path:, fragment: nil, scheme: "file", load_path_entry: nil)
# On Windows, if the path begins with the disk name, we need to add a leading slash to make it a valid URI
Expand Down
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: strict
# frozen_string_literal: true

require "json"

$stdout.binmode
$stdout.sync = true
$stderr.binmode
$stderr.sync = true

module RubyLsp
module TestReporter
class << self
#: (id: String, uri: URI::Generic) -> void
def start_test(id:, uri:)
params = {
id: id,
uri: uri.to_s,
}
send_message("start", params)
end

#: (id: String, uri: URI::Generic) -> void
def record_pass(id:, uri:)
params = {
id: id,
uri: uri.to_s,
}
send_message("pass", params)
end

#: (id: String, message: String, uri: URI::Generic) -> void
def record_fail(id:, message:, uri:)
params = {
id: id,
message: message,
uri: uri.to_s,
}
send_message("fail", params)
end

#: (id: String, message: String?, uri: URI::Generic) -> void
def record_skip(id:, message:, uri:)
params = {
id: id,
message: message,
uri: uri.to_s,
}
send_message("skip", params)
end

#: (id: String, message: String?, uri: String) -> void
def record_error(id:, message:, uri:)
params = {
id: id,
message: message,
uri: uri.to_s,
}
send_message("error", params)
end

private

#: (method_name: String?, params: Hash[String, untyped]) -> void
def send_message(method_name, params)
json_message = { method: method_name, params: params }.to_json
$stdout.write("Content-Length: #{json_message.bytesize}\r\n\r\n#{json_message}")
end
end
end
end
96 changes: 96 additions & 0 deletions lib/ruby_lsp/test_unit_test_runner.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# typed: true
# frozen_string_literal: true

require "test/unit"
require "test/unit/ui/testrunner"
require "ruby_lsp/test_reporter"
require "ruby_indexer/lib/ruby_indexer/uri"

module RubyLsp
class TestRunner < ::Test::Unit::UI::TestRunner
private

#: (::Test::Unit::TestCase test) -> void
def test_started(test)
current_test = test
@current_uri = uri_for_test(current_test)
return unless @current_uri

@current_test_id = "#{current_test.class.name}##{current_test.method_name}"
TestReporter.start_test(
id: @current_test_id,
uri: @current_uri,
)
end

#: (::Test::Unit::TestCase test) -> void
def test_finished(test)
if test.passed?
TestReporter.record_pass(
id: @current_test_id,
uri: @current_uri,
)
end
end

#: (::Test::Unit::Failure | ::Test::Unit::Error | ::Test::Unit::Pending 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)
TestReporter.record_fail(
id: @current_test_id,
message: failure.message,
uri: @current_uri,
)
end

#: (::Test::Unit::Error error) -> void
def record_error(error)
TestReporter.record_error(
id: @current_test_id,
message: error.message,
uri: @current_uri,
)
end

#: (::Test::Unit::Pending pending) -> void
def record_skip(pending)
TestReporter.record_skip(
id: @current_test_id,
message: pending.message,
uri: @current_uri,
)
end

#: (::Test::Unit::TestCase test) -> URI::Generic?
def uri_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?)

absolute_path = File.expand_path(file, Dir.pwd)
URI::Generic.from_path(path: absolute_path)
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 08ec2e7

Please sign in to comment.