Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add prototype for custom test reporter for minitest #3187

Open
wants to merge 19 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ PATH
specs:
ruby-lsp (0.23.12)
language_server-protocol (~> 3.17.0)
minitest-reporters
prism (>= 1.2, < 2.0)
rbs (>= 3, < 4)
sorbet-runtime (>= 0.5.10782)
Expand Down
116 changes: 116 additions & 0 deletions lib/minitest/reporters/ruby_lsp_reporter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# typed: strict
# frozen_string_literal: true

require "sorbet-runtime"
require "ruby_lsp/test_reporter"

# NOTE: minitest-reporters mentioned an API change between minitest 5.10 and 5.11, so we should verify:
# https://github.com/minitest-reporters/minitest-reporters/blob/265ff4b40d5827e84d7e902b808fbee860b61221/lib/minitest/reporters/base_reporter.rb#L82-L91

# TODO: the other reporters call print_info for formatting and backtrace filtering. Look into if we should also do that.

# unless defined?(Minitest::Reporters)
# puts "Minitest::Reporters not defined. Please add `gem 'minitest-reporters'` to your Gemfile."
# exit(1)
# end

require "minitest/reporters"

module Minitest
module Reporters
# TODO: consider if minitest-reporrters should be a dependency of ruby-lsp
class RubyLspReporter < ::Minitest::Reporters::BaseReporter
extend T::Sig

sig { void }
def initialize
@reporting = T.let(RubyLsp::TestReporter.new, RubyLsp::TestReporter)
super
end

sig { params(test: Minitest::Test).void }
def before_test(test)
@reporting.before_test(
id: id_from_test(test),
file: file_for_test(test),
)
super
end

sig { params(test: Minitest::Test).void }
def after_test(test)
@reporting.after_test(
id: id_from_test(test),
file: file_for_test(test),
)
super
end

sig { params(test: Minitest::Result).void }
def record(test)
super

if test.passed?
record_pass(test)
elsif test.skipped?
record_skip(test)
elsif test.failure
record_fail(test)
end
end

sig { params(result: Minitest::Result).void }
def record_pass(result)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is some duplication between the various record_ methods, but I want to understand what's actually required before cleaning this up. We'll know better once integrated with the extension.

info = {
id: id_from_result(result),
file: result.source_location[0],
}
@reporting.record_pass(**info)
end

sig { params(result: Minitest::Result).void }
def record_skip(result)
info = {
id: id_from_result(result),
message: result.failure.message,
file: result.source_location[0],
}
@reporting.record_skip(**info)
end

sig { params(result: Minitest::Result).void }
def record_fail(result)
info = {
id: id_from_result(result),
type: result.failure.class.name,
message: result.failure.message,
file: result.source_location[0],
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These record events seem to have access to the file path without having to resort to Module.const_source_location. Is there some other event, like start_suite that we can remember the file path ahead of time?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a before_suite which takes a Minitest::Reporters::Suite, but that doesn't have file information:

https://github.com/minitest-reporters/minitest-reporters/blob/265ff4b40d5827e84d7e902b808fbee860b61221/lib/minitest/reporters/base_reporter.rb#L3C11-L3C16

We could propose adding this in minitest-reporters.

}
@reporting.record_fail(**info)
end

private

sig { params(test: Minitest::Test).returns(String) }
def id_from_test(test)
[test.class.name, test.name].join("#")
end

sig { params(result: Minitest::Result).returns(String) }
def id_from_result(result)
[result.klass, result.name].join("#")
end

sig { params(test: Minitest::Test).returns(String) }
def file_for_test(test)
location = Kernel.const_source_location(test.class_name)
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
end
end
end
7 changes: 7 additions & 0 deletions lib/ruby-lsp.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
# typed: true
# frozen_string_literal: true

require "minitest/reporters/ruby_lsp_reporter"
Minitest::Reporters.use!(Minitest::Reporters::RubyLspReporter.new)

module RubyLsp
VERSION = File.read(File.expand_path("../VERSION", __dir__)).strip
end

# # Temporary for verification
# if ENV["RUBY_LSP"]
# end
86 changes: 86 additions & 0 deletions lib/ruby_lsp/test_reporter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# typed: strict
# frozen_string_literal: true

require "json"

module RubyLsp
class TestReporter
extend T::Sig

sig { params(io: IO).void }
def initialize(io: $stdout)
@io = io
end

sig { params(id: String, file: String).void }
def before_test(id:, file:)
result = {
event: "before_test",
id: id,
file: file,
}
puts result
end

sig { params(id: String, file: String).void }
def after_test(id:, file:)
result = {
event: "after_test",
id: id,
file: file,
}
puts result
end

sig { params(id: String, file: String).void }
def record_pass(id:, file:)
result = {
event: "pass",
id: id,
file: file,
}
puts result
end

sig do
params(
id: String,
type: T.untyped, # TODO: what type should this be?
message: String,
file: String,
).void
end
def record_fail(id:, type:, message:, file:)
result = {
event: "fail",
type: type,
message: message,
id: id,
file: file,
}
puts result
end

sig { params(id: String, message: T.nilable(String), file: String).void }
def record_skip(id:, message:, file:)
result = {
event: "skip",
message: message,
id: id,
file: file,
}
puts result
end

sig { params(result: T::Hash[Symbol, T.untyped]).void }
def puts(result)
io.puts result.to_json
io.flush
end

private

sig { returns(IO) }
attr_reader :io
end
end
1 change: 1 addition & 0 deletions ruby-lsp.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Gem::Specification.new do |s|

# Dependencies must be kept in sync with the checks in the extension side on workspace.ts
s.add_dependency("language_server-protocol", "~> 3.17.0")
s.add_dependency("minitest-reporters") # version?
s.add_dependency("prism", ">= 1.2", "< 2.0")
s.add_dependency("rbs", ">= 3", "< 4")
s.add_dependency("sorbet-runtime", ">= 0.5.10782")
Expand Down